From 6e8d0ded36f4223f3dc2fc4ee830ba59493f19c5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 20 Mar 2024 12:59:02 +0100 Subject: [PATCH 01/21] Room Directory Search : setup the feature --- features/roomdirectory/api/build.gradle.kts | 28 ++++++++ .../api/RoomDirectoryEntryPoint.kt | 38 +++++++++++ features/roomdirectory/impl/build.gradle.kts | 52 +++++++++++++++ .../impl/DefaultRoomDirectoryEntryPoint.kt | 46 +++++++++++++ .../impl/RoomDirectoryFlowNode.kt | 66 +++++++++++++++++++ .../impl/search/RoomDirectorySearchEvents.kt | 25 +++++++ .../impl/search/RoomDirectorySearchNode.kt | 44 +++++++++++++ .../search/RoomDirectorySearchPresenter.kt | 40 +++++++++++ .../impl/search/RoomDirectorySearchState.kt | 21 ++++++ .../RoomDirectorySearchStateProvider.kt | 31 +++++++++ .../impl/search/RoomDirectorySearchView.kt | 48 ++++++++++++++ .../libraries/featureflag/api/FeatureFlags.kt | 7 ++ .../impl/StaticFeatureFlagProvider.kt | 1 + 13 files changed, 447 insertions(+) create mode 100644 features/roomdirectory/api/build.gradle.kts create mode 100644 features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt create mode 100644 features/roomdirectory/impl/build.gradle.kts create mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt create mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/RoomDirectoryFlowNode.kt create mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchEvents.kt create mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchNode.kt create mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchPresenter.kt create mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchState.kt create mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchStateProvider.kt create mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchView.kt diff --git a/features/roomdirectory/api/build.gradle.kts b/features/roomdirectory/api/build.gradle.kts new file mode 100644 index 0000000000..c55c647e24 --- /dev/null +++ b/features/roomdirectory/api/build.gradle.kts @@ -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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.roomdirectory.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) +} diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt new file mode 100644 index 0000000000..033ec67b33 --- /dev/null +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt @@ -0,0 +1,38 @@ +/* + * 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 com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId + +interface RoomDirectoryEntryPoint : FeatureEntryPoint { + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onRoomJoined(roomId: RoomId) + } +} + diff --git a/features/roomdirectory/impl/build.gradle.kts b/features/roomdirectory/impl/build.gradle.kts new file mode 100644 index 0000000000..0e35623401 --- /dev/null +++ b/features/roomdirectory/impl/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * 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. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.roomdirectory.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + api(projects.features.roomdirectory.api) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + + ksp(libs.showkase.processor) +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt new file mode 100644 index 0000000000..ac085b4d03 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt @@ -0,0 +1,46 @@ +/* + * 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 + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultRoomDirectoryEntryPoint @Inject constructor() : RoomDirectoryEntryPoint { + + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDirectoryEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : RoomDirectoryEntryPoint.NodeBuilder { + + override fun callback(callback: RoomDirectoryEntryPoint.Callback): RoomDirectoryEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/RoomDirectoryFlowNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/RoomDirectoryFlowNode.kt new file mode 100644 index 0000000000..e053fa6c58 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/RoomDirectoryFlowNode.kt @@ -0,0 +1,66 @@ +/* + * 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 + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +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.features.roomdirectory.impl.search.RoomDirectorySearchNode +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.SessionScope +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +class RoomDirectoryFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +) { + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Root -> { + createNode(buildContext) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView() + } +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchEvents.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchEvents.kt new file mode 100644 index 0000000000..f7212529b2 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchEvents.kt @@ -0,0 +1,25 @@ +/* + * 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.search + +import io.element.android.libraries.matrix.api.core.RoomId + +sealed interface RoomDirectorySearchEvents { + data class Search(val query: String) : RoomDirectorySearchEvents + data object LoadMore : RoomDirectorySearchEvents + data class JoinRoom(val roomId: RoomId) : RoomDirectorySearchEvents +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchNode.kt new file mode 100644 index 0000000000..1f459f404c --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchNode.kt @@ -0,0 +1,44 @@ +/* + * 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.search + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class RoomDirectorySearchNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: RoomDirectorySearchPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + RoomDirectorySearchView( + state = state, + modifier = modifier + ) + } +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchPresenter.kt new file mode 100644 index 0000000000..d0cc6d47c4 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchPresenter.kt @@ -0,0 +1,40 @@ +/* + * 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.search + +import androidx.compose.runtime.Composable +import io.element.android.libraries.architecture.Presenter +import javax.inject.Inject + +class RoomDirectorySearchPresenter @Inject constructor() : Presenter { + + @Composable + override fun present(): RoomDirectorySearchState { + + fun handleEvents(event: RoomDirectorySearchEvents) { + when (event) { + is RoomDirectorySearchEvents.JoinRoom -> TODO() + RoomDirectorySearchEvents.LoadMore -> TODO() + is RoomDirectorySearchEvents.Search -> TODO() + } + } + + return RoomDirectorySearchState( + eventSink = ::handleEvents + ) + } +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchState.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchState.kt new file mode 100644 index 0000000000..0e77801e12 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchState.kt @@ -0,0 +1,21 @@ +/* + * 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.search + +data class RoomDirectorySearchState( + val eventSink: (RoomDirectorySearchEvents) -> Unit +) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchStateProvider.kt new file mode 100644 index 0000000000..31464fd17c --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchStateProvider.kt @@ -0,0 +1,31 @@ +/* + * 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.search + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class RoomDirectorySearchStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomDirectorySearchState(), + // Add other states here + ) +} + +fun aRoomDirectorySearchState() = RoomDirectorySearchState( + eventSink = {} +) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchView.kt new file mode 100644 index 0000000000..d2e3ad1ec9 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchView.kt @@ -0,0 +1,48 @@ +/* + * 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.search + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun RoomDirectorySearchView( + state: RoomDirectorySearchState, + modifier: Modifier = Modifier, +) { + Box(modifier, contentAlignment = Alignment.Center) { + Text( + "RoomDirectorySearch feature view", + color = MaterialTheme.colorScheme.primary, + ) + } +} + +@PreviewsDayNight +@Composable +fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectorySearchState) = ElementPreview { + RoomDirectorySearchView( + state = state, + ) +} diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index a242d014d1..7140a89064 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -89,4 +89,11 @@ enum class FeatureFlags( defaultValue = true, isFinished = false, ), + RoomDirectorySearch( + key = "feature.roomdirectorysearch", + title = "Room directory search", + description = "Allow user to search for public rooms in his homeserver", + defaultValue = true, + isFinished = false, + ) } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt index 13cc7da462..5b6ee3bc03 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt @@ -42,6 +42,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.MarkAsUnread -> true FeatureFlags.RoomListFilters -> false FeatureFlags.RoomModeration -> false + FeatureFlags.RoomDirectorySearch -> false } } else { false From a8028ba9684d3bea08c13cfd1c194060c5f01a93 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 20 Mar 2024 15:35:54 +0100 Subject: [PATCH 02/21] Room directory search : branch entry point --- .../android/appnav/LoggedInFlowNode.kt | 18 +++++++++++ .../roomlist/api/RoomListEntryPoint.kt | 1 + .../features/roomlist/impl/RoomListNode.kt | 5 +++ .../features/roomlist/impl/RoomListView.kt | 3 ++ .../impl/search/RoomListSearchView.kt | 31 ++++++++++++++++++- 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index e4290d5bdf..9a96a01d27 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -53,6 +53,7 @@ import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.PreferencesEntryPoint +import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint @@ -97,6 +98,7 @@ class LoggedInFlowNode @AssistedInject constructor( private val ftueState: FtueState, private val lockScreenEntryPoint: LockScreenEntryPoint, private val lockScreenStateService: LockScreenService, + private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint, private val matrixClient: MatrixClient, snackbarDispatcher: SnackbarDispatcher, ) : BaseFlowNode( @@ -225,6 +227,9 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data object Ftue : NavTarget + + @Parcelize + data object RoomDirectorySearch : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -270,6 +275,10 @@ class LoggedInFlowNode @AssistedInject constructor( override fun onReportBugClicked() { plugins().forEach { it.onOpenBugReport() } } + + override fun onRoomDirectorySearchClicked() { + backstack.push(NavTarget.RoomDirectorySearch) + } } roomListEntryPoint .nodeBuilder(this, buildContext) @@ -377,6 +386,15 @@ class LoggedInFlowNode @AssistedInject constructor( }) .build() } + NavTarget.RoomDirectorySearch -> { + roomDirectoryEntryPoint.nodeBuilder(this, buildContext) + .callback(object : RoomDirectoryEntryPoint.Callback { + override fun onRoomJoined(roomId: RoomId) { + coroutineScope.launch { attachRoom(roomId) } + } + }) + .build() + } } } diff --git a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt index 0404ce18fc..c3b2ff36a2 100644 --- a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt +++ b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt @@ -38,5 +38,6 @@ interface RoomListEntryPoint : FeatureEntryPoint { fun onInvitesClicked() fun onRoomSettingsClicked(roomId: RoomId) fun onReportBugClicked() + fun onRoomDirectorySearchClicked() } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt index 5dec53e158..a9740bcfcb 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt @@ -91,6 +91,10 @@ class RoomListNode @AssistedInject constructor( } } + private fun onRoomDirectorySearchClicked() { + plugins().forEach { it.onRoomDirectorySearchClicked() } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -105,6 +109,7 @@ class RoomListNode @AssistedInject constructor( onInvitesClicked = this::onInvitesClicked, onRoomSettingsClicked = this::onRoomSettingsClicked, onMenuActionClicked = { onMenuActionClicked(activity, it) }, + onRoomDirectorySearchClicked = this::onRoomDirectorySearchClicked, modifier = modifier, ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index 23a105f143..e0a3fc5201 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -59,6 +59,7 @@ fun RoomListView( onInvitesClicked: () -> Unit, onRoomSettingsClicked: (roomId: RoomId) -> Unit, onMenuActionClicked: (RoomListMenuAction) -> Unit, + onRoomDirectorySearchClicked: () -> Unit, modifier: Modifier = Modifier, ) { ConnectivityIndicatorContainer( @@ -99,6 +100,7 @@ fun RoomListView( state = state.searchState, onRoomClicked = onRoomClicked, onRoomLongClicked = { onRoomLongClicked(it) }, + onRoomDirectorySearchClicked = onRoomDirectorySearchClicked, modifier = Modifier .statusBarsPadding() .padding(top = topPadding) @@ -197,5 +199,6 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class) onInvitesClicked = {}, onRoomSettingsClicked = {}, onMenuActionClicked = {}, + onRoomDirectorySearchClicked = {}, ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt index eff6449449..e8c68cebf0 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt @@ -26,6 +26,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.QrCode import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextFieldDefaults @@ -50,8 +52,10 @@ import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.modifiers.applyIf import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -64,6 +68,7 @@ internal fun RoomListSearchView( state: RoomListSearchState, onRoomClicked: (RoomId) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, + onRoomDirectorySearchClicked: () -> Unit, modifier: Modifier = Modifier, ) { BackHandler(enabled = state.isSearchActive) { @@ -87,6 +92,7 @@ internal fun RoomListSearchView( state = state, onRoomClicked = onRoomClicked, onRoomLongClicked = onRoomLongClicked, + onRoomDirectorySearchClicked = onRoomDirectorySearchClicked, ) } } @@ -99,6 +105,7 @@ private fun RoomListSearchContent( state: RoomListSearchState, onRoomClicked: (RoomId) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, + onRoomDirectorySearchClicked: () -> Unit, ) { val borderColor = MaterialTheme.colorScheme.tertiary val strokeWidth = 1.dp @@ -169,6 +176,14 @@ private fun RoomListSearchContent( .padding(padding) .consumeWindowInsets(padding) ) { + if(state.query.isEmpty()){ + RoomDirectorySearchButton( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp, horizontal = 16.dp), + onClick = onRoomDirectorySearchClicked + ) + } LazyColumn( modifier = Modifier.weight(1f), ) { @@ -187,12 +202,26 @@ private fun RoomListSearchContent( } } +@Composable +private fun RoomDirectorySearchButton( + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Button( + text = "Room directory", + leadingIcon = IconSource.Vector(CompoundIcons.ListBulleted()), + onClick = onClick, + modifier = modifier, + ) +} + @PreviewsDayNight @Composable internal fun RoomListSearchResultContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview { RoomListSearchContent( state = state, onRoomClicked = {}, - onRoomLongClicked = {} + onRoomLongClicked = {}, + onRoomDirectorySearchClicked = {}, ) } From b0894fcd115c7b52dfafe8b24fd2d10bda3bf236 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 20 Mar 2024 18:32:41 +0100 Subject: [PATCH 03/21] Room directory search : start implementing ui with fake data --- features/roomdirectory/impl/build.gradle.kts | 1 + .../impl/search/RoomDirectorySearchNode.kt | 1 + .../search/RoomDirectorySearchPresenter.kt | 49 ++++- .../impl/search/RoomDirectorySearchState.kt | 5 + .../RoomDirectorySearchStateProvider.kt | 45 +++- .../impl/search/RoomDirectorySearchView.kt | 203 +++++++++++++++++- .../RoomDirectorySearchDataSource.kt | 80 +++++++ .../search/model/RoomDirectorySearchResult.kt | 28 +++ .../components/avatar/AvatarSize.kt | 4 +- 9 files changed, 401 insertions(+), 15 deletions(-) create mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/datasource/RoomDirectorySearchDataSource.kt create mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/model/RoomDirectorySearchResult.kt diff --git a/features/roomdirectory/impl/build.gradle.kts b/features/roomdirectory/impl/build.gradle.kts index 0e35623401..9d56802700 100644 --- a/features/roomdirectory/impl/build.gradle.kts +++ b/features/roomdirectory/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchNode.kt index 1f459f404c..1239dc5fab 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchNode.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchNode.kt @@ -38,6 +38,7 @@ class RoomDirectorySearchNode @AssistedInject constructor( val state = presenter.present() RoomDirectorySearchView( state = state, + onBackPressed = ::navigateUp, modifier = modifier ) } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchPresenter.kt index d0cc6d47c4..e0d6a9dbf4 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchPresenter.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchPresenter.kt @@ -17,24 +17,65 @@ package io.element.android.features.roomdirectory.impl.search import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.features.roomdirectory.impl.search.datasource.RoomDirectorySearchDataSource import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject -class RoomDirectorySearchPresenter @Inject constructor() : Presenter { +class RoomDirectorySearchPresenter @Inject constructor( + private val client: MatrixClient, + private val dataSource: RoomDirectorySearchDataSource, +) : Presenter { @Composable override fun present(): RoomDirectorySearchState { + var searchQuery by rememberSaveable { + mutableStateOf("") + } + + val results by dataSource.searchResults.collectAsState() + + val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(searchQuery) { + dataSource.updateSearchQuery(searchQuery) + } + fun handleEvents(event: RoomDirectorySearchEvents) { when (event) { - is RoomDirectorySearchEvents.JoinRoom -> TODO() - RoomDirectorySearchEvents.LoadMore -> TODO() - is RoomDirectorySearchEvents.Search -> TODO() + is RoomDirectorySearchEvents.JoinRoom -> { + coroutineScope.joinRoom(event.roomId) + } + RoomDirectorySearchEvents.LoadMore -> { + coroutineScope.launch { + dataSource.loadMore() + } + } + is RoomDirectorySearchEvents.Search -> { + searchQuery = event.query + } } } return RoomDirectorySearchState( + query = searchQuery, + results = results, eventSink = ::handleEvents ) } + + private fun CoroutineScope.joinRoom(roomId: RoomId) = launch { + client.getRoom(roomId)?.join() + } } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchState.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchState.kt index 0e77801e12..e1aa7dc832 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchState.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchState.kt @@ -16,6 +16,11 @@ package io.element.android.features.roomdirectory.impl.search +import io.element.android.features.roomdirectory.impl.search.model.RoomDirectorySearchResult +import kotlinx.collections.immutable.ImmutableList + data class RoomDirectorySearchState( + val query: String, + val results: ImmutableList, val eventSink: (RoomDirectorySearchEvents) -> Unit ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchStateProvider.kt index 31464fd17c..3dd56c77bb 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchStateProvider.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchStateProvider.kt @@ -17,15 +17,54 @@ package io.element.android.features.roomdirectory.impl.search import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.roomdirectory.impl.search.model.RoomDirectorySearchResult +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 kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf open class RoomDirectorySearchStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomDirectorySearchState(), - // Add other states here + aRoomDirectorySearchState( + query = "Element", + results = persistentListOf( + RoomDirectorySearchResult( + roomId = RoomId("@exa:matrix.org"), + name = "Element X Android", + description = "Element X is a secure, private and decentralized messenger.", + avatarData = AvatarData( + id = "@exa:matrix.org", + name = "Element X Android", + url = null, + size = AvatarSize.RoomDirectorySearchItem + ), + canBeJoined = true, + ), + RoomDirectorySearchResult( + roomId = RoomId("@exi:matrix.org"), + name = "Element X iOS", + description = "Element X is a secure, private and decentralized messenger.", + avatarData = AvatarData( + id = "@exi:matrix.org", + name = "Element X iOS", + url = null, + size = AvatarSize.RoomDirectorySearchItem + ), + canBeJoined = false, + ) + ) + ), ) } -fun aRoomDirectorySearchState() = RoomDirectorySearchState( - eventSink = {} +fun aRoomDirectorySearchState( + query: String = "", + results: ImmutableList = persistentListOf(), +) = RoomDirectorySearchState( + query = query, + results = results, + eventSink = { } ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchView.kt index d2e3ad1ec9..2af471c51f 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchView.kt @@ -16,27 +16,215 @@ package io.element.android.features.roomdirectory.impl.search -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow 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.search.model.RoomDirectorySearchResult +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +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.TextButton +import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun RoomDirectorySearchView( state: RoomDirectorySearchState, + onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { - Box(modifier, contentAlignment = Alignment.Center) { - Text( - "RoomDirectorySearch feature view", - color = MaterialTheme.colorScheme.primary, - ) + + fun onQueryChanged(query: String) { + state.eventSink(RoomDirectorySearchEvents.Search(query)) } + + Scaffold( + modifier = modifier, + topBar = { + RoomDirectorySearchTopBar( + query = state.query, + onQueryChanged = ::onQueryChanged, + onBackPressed = onBackPressed, + ) + }, + content = { padding -> + RoomDirectorySearchContent( + state = state, + onResultClicked = { roomId -> + state.eventSink(RoomDirectorySearchEvents.JoinRoom(roomId)) + }, + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + ) + } + ) +} + +@Composable +private fun RoomDirectorySearchContent( + state: RoomDirectorySearchState, + onResultClicked: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier = modifier) { + items(state.results) { result -> + RoomDirectorySearchResultRow( + result = result, + onClick = onResultClicked, + ) + } + } +} + +@Composable +private fun RoomDirectorySearchResultRow( + result: RoomDirectorySearchResult, + onClick: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable { onClick(result.roomId) } + .padding( + top = 12.dp, + bottom = 12.dp, + start = 16.dp, + ) + .height(IntrinsicSize.Min), + ) { + Avatar( + avatarData = result.avatarData, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp) + ) { + Text( + text = result.name, + maxLines = 1, + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPrimary, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = result.description, + maxLines = 1, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + overflow = TextOverflow.Ellipsis, + ) + } + if (result.canBeJoined) { + CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textSuccessPrimary) { + TextButton( + text = stringResource(id = CommonStrings.action_join), + onClick = { onClick(result.roomId) }, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 4.dp, end = 12.dp) + ) + } + } else { + Spacer(modifier = Modifier.width(24.dp)) + } + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun RoomDirectorySearchTopBar( + query: String, + onQueryChanged: (String) -> Unit, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + val borderColor = ElementTheme.colors.borderInteractivePrimary + val borderStroke = 1.dp + TopAppBar( + modifier = modifier.drawBehind { + drawLine( + color = borderColor, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = borderStroke.value + ) + }, + title = { + val focusRequester = FocusRequester() + TextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + value = query, + singleLine = true, + onValueChange = onQueryChanged, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + ), + trailingIcon = { + if (query.isNotEmpty()) { + IconButton(onClick = { + onQueryChanged("") + }) { + Icon( + imageVector = CompoundIcons.Close(), + contentDescription = stringResource(CommonStrings.action_cancel), + ) + } + } + } + ) + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + ) } @PreviewsDayNight @@ -44,5 +232,6 @@ fun RoomDirectorySearchView( fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectorySearchState) = ElementPreview { RoomDirectorySearchView( state = state, + onBackPressed = {}, ) } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/datasource/RoomDirectorySearchDataSource.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/datasource/RoomDirectorySearchDataSource.kt new file mode 100644 index 0000000000..6780c97017 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/datasource/RoomDirectorySearchDataSource.kt @@ -0,0 +1,80 @@ +/* + * 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.search.datasource + +import io.element.android.features.roomdirectory.impl.search.model.RoomDirectorySearchResult +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 kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +class RoomDirectorySearchDataSource @Inject constructor( + +) { + + private val _searchResults = MutableStateFlow>(persistentListOf()) + + suspend fun updateSearchQuery(searchQuery: String) { + //TODO branch to matrix sdk + if (searchQuery.isEmpty()) { + _searchResults.value = persistentListOf() + } else { + delay(100) + emitFakeResults() + } + } + + suspend fun loadMore() { + //TODO branch to matrix sdk + } + + private fun emitFakeResults() { + _searchResults.value = persistentListOf( + RoomDirectorySearchResult( + roomId = RoomId("!exa:matrix.org"), + name = "Element X Android", + description = "Element X is a secure, private and decentralized messenger.", + avatarData = AvatarData( + id = "!exa:matrix.org", + name = "Element X Android", + url = null, + size = AvatarSize.RoomDirectorySearchItem + ), + canBeJoined = true, + ), + RoomDirectorySearchResult( + roomId = RoomId("!exi:matrix.org"), + name = "Element X iOS", + description = "Element X is a secure, private and decentralized messenger.", + avatarData = AvatarData( + id = "!exi:matrix.org", + name = "Element X iOS", + url = null, + size = AvatarSize.RoomDirectorySearchItem + ), + canBeJoined = false, + ) + ) + } + + val searchResults: StateFlow> = _searchResults +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/model/RoomDirectorySearchResult.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/model/RoomDirectorySearchResult.kt new file mode 100644 index 0000000000..ef531e716d --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/model/RoomDirectorySearchResult.kt @@ -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.impl.search.model + +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.matrix.api.core.RoomId + +data class RoomDirectorySearchResult( + val roomId: RoomId, + val name: String, + val description: String, + val avatarData: AvatarData, + val canBeJoined: Boolean, +) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 0451cb5839..382aac5468 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -51,5 +51,7 @@ enum class AvatarSize(val dp: Dp) { NotificationsOptIn(32.dp), - CustomRoomNotificationSetting(36.dp) + CustomRoomNotificationSetting(36.dp), + + RoomDirectorySearchItem(36.dp), } From fa7a889a3f094638f4ff7b70db42840e0ae0e300 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 21 Mar 2024 17:22:00 +0100 Subject: [PATCH 04/21] Room directory : change names and adapt ui --- .../impl/RoomDirectoryFlowNode.kt | 4 +- .../RoomDirectoryEvents.kt} | 11 +- .../RoomDirectoryNode.kt} | 8 +- .../RoomDirectoryPresenter.kt} | 38 +++-- .../RoomDirectoryState.kt} | 13 +- .../RoomDirectoryStateProvider.kt} | 33 +++-- .../RoomDirectoryView.kt} | 136 ++++++++++++------ .../datasource/RoomDirectoryDataSource.kt} | 31 ++-- .../model/RoomDirectoryRoomSummary.kt} | 4 +- .../impl/search/RoomListSearchView.kt | 2 +- 10 files changed, 176 insertions(+), 104 deletions(-) rename features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/{search/RoomDirectorySearchEvents.kt => root/RoomDirectoryEvents.kt} (65%) rename features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/{search/RoomDirectorySearchNode.kt => root/RoomDirectoryNode.kt} (86%) rename features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/{search/RoomDirectorySearchPresenter.kt => root/RoomDirectoryPresenter.kt} (63%) rename features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/{search/RoomDirectorySearchState.kt => root/RoomDirectoryState.kt} (57%) rename features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/{search/RoomDirectorySearchStateProvider.kt => root/RoomDirectoryStateProvider.kt} (71%) rename features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/{search/RoomDirectorySearchView.kt => root/RoomDirectoryView.kt} (69%) rename features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/{search/datasource/RoomDirectorySearchDataSource.kt => root/datasource/RoomDirectoryDataSource.kt} (75%) rename features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/{search/model/RoomDirectorySearchResult.kt => root/model/RoomDirectoryRoomSummary.kt} (89%) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/RoomDirectoryFlowNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/RoomDirectoryFlowNode.kt index e053fa6c58..244e61b3b9 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/RoomDirectoryFlowNode.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/RoomDirectoryFlowNode.kt @@ -26,7 +26,7 @@ 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.features.roomdirectory.impl.search.RoomDirectorySearchNode +import io.element.android.features.roomdirectory.impl.root.RoomDirectoryNode import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode @@ -54,7 +54,7 @@ class RoomDirectoryFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Root -> { - createNode(buildContext) + createNode(buildContext) } } } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchEvents.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt similarity index 65% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchEvents.kt rename to features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt index f7212529b2..267c889f61 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchEvents.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt @@ -14,12 +14,13 @@ * limitations under the License. */ -package io.element.android.features.roomdirectory.impl.search +package io.element.android.features.roomdirectory.impl.root import io.element.android.libraries.matrix.api.core.RoomId -sealed interface RoomDirectorySearchEvents { - data class Search(val query: String) : RoomDirectorySearchEvents - data object LoadMore : RoomDirectorySearchEvents - data class JoinRoom(val roomId: RoomId) : RoomDirectorySearchEvents +sealed interface RoomDirectoryEvents { + data class Search(val query: String) : RoomDirectoryEvents + data object LoadMore : RoomDirectoryEvents + data class JoinRoom(val roomId: RoomId) : RoomDirectoryEvents + data class SearchActiveChange(val isActive: Boolean) : RoomDirectoryEvents } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt similarity index 86% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchNode.kt rename to features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt index 1239dc5fab..0da712eefe 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchNode.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.roomdirectory.impl.search +package io.element.android.features.roomdirectory.impl.root import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -27,16 +27,16 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) -class RoomDirectorySearchNode @AssistedInject constructor( +class RoomDirectoryNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: RoomDirectorySearchPresenter, + private val presenter: RoomDirectoryPresenter, ) : Node(buildContext, plugins = plugins) { @Composable override fun View(modifier: Modifier) { val state = presenter.present() - RoomDirectorySearchView( + RoomDirectoryView( state = state, onBackPressed = ::navigateUp, modifier = modifier diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt similarity index 63% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchPresenter.kt rename to features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt index e0d6a9dbf4..5ab2dfe6eb 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchPresenter.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.roomdirectory.impl.search +package io.element.android.features.roomdirectory.impl.root import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -24,27 +24,31 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import io.element.android.features.roomdirectory.impl.search.datasource.RoomDirectorySearchDataSource +import io.element.android.features.roomdirectory.impl.root.datasource.RoomDirectoryDataSource import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject -class RoomDirectorySearchPresenter @Inject constructor( +class RoomDirectoryPresenter @Inject constructor( private val client: MatrixClient, - private val dataSource: RoomDirectorySearchDataSource, -) : Presenter { + private val dataSource: RoomDirectoryDataSource, +) : Presenter { @Composable - override fun present(): RoomDirectorySearchState { + override fun present(): RoomDirectoryState { var searchQuery by rememberSaveable { mutableStateOf("") } + var isSearchActive by rememberSaveable { + mutableStateOf(false) + } - val results by dataSource.searchResults.collectAsState() + val roomSummaries by dataSource.all.collectAsState() val coroutineScope = rememberCoroutineScope() @@ -52,25 +56,33 @@ class RoomDirectorySearchPresenter @Inject constructor( dataSource.updateSearchQuery(searchQuery) } - fun handleEvents(event: RoomDirectorySearchEvents) { + fun handleEvents(event: RoomDirectoryEvents) { when (event) { - is RoomDirectorySearchEvents.JoinRoom -> { + is RoomDirectoryEvents.JoinRoom -> { coroutineScope.joinRoom(event.roomId) } - RoomDirectorySearchEvents.LoadMore -> { + RoomDirectoryEvents.LoadMore -> { coroutineScope.launch { dataSource.loadMore() } } - is RoomDirectorySearchEvents.Search -> { + is RoomDirectoryEvents.Search -> { searchQuery = event.query } + is RoomDirectoryEvents.SearchActiveChange -> { + isSearchActive = event.isActive + if (!isSearchActive) { + searchQuery = "" + } + } } } - return RoomDirectorySearchState( + return RoomDirectoryState( query = searchQuery, - results = results, + isSearchActive = isSearchActive, + roomSummaries = roomSummaries, + searchResults = SearchBarResultState.Initial(), eventSink = ::handleEvents ) } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchState.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt similarity index 57% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchState.kt rename to features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt index e1aa7dc832..3abf725a24 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchState.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt @@ -14,13 +14,16 @@ * limitations under the License. */ -package io.element.android.features.roomdirectory.impl.search +package io.element.android.features.roomdirectory.impl.root -import io.element.android.features.roomdirectory.impl.search.model.RoomDirectorySearchResult +import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryRoomSummary +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import kotlinx.collections.immutable.ImmutableList -data class RoomDirectorySearchState( +data class RoomDirectoryState( val query: String, - val results: ImmutableList, - val eventSink: (RoomDirectorySearchEvents) -> Unit + val roomSummaries: ImmutableList, + val searchResults: SearchBarResultState>, + val isSearchActive: Boolean, + val eventSink: (RoomDirectoryEvents) -> Unit ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt similarity index 71% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchStateProvider.kt rename to features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt index 3dd56c77bb..a9d1f2287f 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchStateProvider.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt @@ -14,24 +14,25 @@ * limitations under the License. */ -package io.element.android.features.roomdirectory.impl.search +package io.element.android.features.roomdirectory.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.roomdirectory.impl.search.model.RoomDirectorySearchResult +import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryRoomSummary import io.element.android.libraries.designsystem.components.avatar.AvatarData 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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -open class RoomDirectorySearchStateProvider : PreviewParameterProvider { - override val values: Sequence +open class RoomDirectorySearchStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - aRoomDirectorySearchState(), - aRoomDirectorySearchState( + aRoomDirectoryState(), + aRoomDirectoryState( query = "Element", - results = persistentListOf( - RoomDirectorySearchResult( + roomSummaries = persistentListOf( + RoomDirectoryRoomSummary( roomId = RoomId("@exa:matrix.org"), name = "Element X Android", description = "Element X is a secure, private and decentralized messenger.", @@ -43,7 +44,7 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider = persistentListOf(), -) = RoomDirectorySearchState( + isSearchActive: Boolean = false, + roomSummaries: ImmutableList = persistentListOf(), + searchResults: SearchBarResultState> = SearchBarResultState.Initial(), +) = RoomDirectoryState( query = query, - results = results, - eventSink = { } + isSearchActive = isSearchActive, + roomSummaries = roomSummaries, + searchResults = searchResults, + eventSink = {}, ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt similarity index 69% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchView.kt rename to features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index 2af471c51f..b79e4cfd20 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/RoomDirectorySearchView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -14,14 +14,16 @@ * limitations under the License. */ -package io.element.android.features.roomdirectory.impl.search +package io.element.android.features.roomdirectory.impl.root import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -29,10 +31,8 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -47,46 +47,39 @@ 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.search.model.RoomDirectorySearchResult +import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryRoomSummary import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.SearchBar import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList @Composable -fun RoomDirectorySearchView( - state: RoomDirectorySearchState, +fun RoomDirectoryView( + state: RoomDirectoryState, onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { - - fun onQueryChanged(query: String) { - state.eventSink(RoomDirectorySearchEvents.Search(query)) - } - Scaffold( modifier = modifier, topBar = { - RoomDirectorySearchTopBar( - query = state.query, - onQueryChanged = ::onQueryChanged, - onBackPressed = onBackPressed, - ) + RoomDirectoryTopBar(onBackPressed = onBackPressed) }, content = { padding -> - RoomDirectorySearchContent( + RoomDirectoryContent( state = state, onResultClicked = { roomId -> - state.eventSink(RoomDirectorySearchEvents.JoinRoom(roomId)) + state.eventSink(RoomDirectoryEvents.JoinRoom(roomId)) }, modifier = Modifier .padding(padding) @@ -96,16 +89,75 @@ fun RoomDirectorySearchView( ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun RoomDirectorySearchContent( - state: RoomDirectorySearchState, +private fun RoomDirectoryTopBar( + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + navigationIcon = { + BackButton(onClick = onBackPressed) + }, + title = { + Text( + text = "Room directory", + style = ElementTheme.typography.aliasScreenTitle, + ) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RoomDirectoryContent( + state: RoomDirectoryState, onResultClicked: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { - LazyColumn(modifier = modifier) { - items(state.results) { result -> - RoomDirectorySearchResultRow( - result = result, + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + SearchBar( + query = state.query, + onQueryChange = { query -> + state.eventSink(RoomDirectoryEvents.Search(query)) + }, + active = state.isSearchActive, + onActiveChange = { + state.eventSink(RoomDirectoryEvents.SearchActiveChange(it)) + }, + resultState = state.searchResults, + placeHolderTitle = stringResource(id = CommonStrings.action_search), + ) { results -> + RoomDirectoryRoomList( + rooms = results, + onResultClicked = onResultClicked, + ) + } + if (!state.isSearchActive) { + RoomDirectoryRoomList( + rooms = state.roomSummaries, + onResultClicked = onResultClicked, + ) + } + } +} + +@Composable +private fun RoomDirectoryRoomList( + rooms: ImmutableList, + onResultClicked: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + ) { + items(rooms) { room -> + RoomDirectoryRoomRow( + room = room, onClick = onResultClicked, ) } @@ -113,15 +165,15 @@ private fun RoomDirectorySearchContent( } @Composable -private fun RoomDirectorySearchResultRow( - result: RoomDirectorySearchResult, +private fun RoomDirectoryRoomRow( + room: RoomDirectoryRoomSummary, onClick: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier .fillMaxWidth() - .clickable { onClick(result.roomId) } + .clickable { onClick(room.roomId) } .padding( top = 12.dp, bottom = 12.dp, @@ -130,7 +182,7 @@ private fun RoomDirectorySearchResultRow( .height(IntrinsicSize.Min), ) { Avatar( - avatarData = result.avatarData, + avatarData = room.avatarData, modifier = Modifier.align(Alignment.CenterVertically) ) Column( @@ -139,30 +191,28 @@ private fun RoomDirectorySearchResultRow( .padding(start = 16.dp) ) { Text( - text = result.name, + text = room.name, maxLines = 1, style = ElementTheme.typography.fontBodyLgRegular, color = ElementTheme.colors.textPrimary, overflow = TextOverflow.Ellipsis, ) Text( - text = result.description, + text = room.description, maxLines = 1, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textSecondary, overflow = TextOverflow.Ellipsis, ) } - if (result.canBeJoined) { - CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textSuccessPrimary) { - TextButton( - text = stringResource(id = CommonStrings.action_join), - onClick = { onClick(result.roomId) }, - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 4.dp, end = 12.dp) - ) - } + if (room.canBeJoined) { + Text( + text = stringResource(id = CommonStrings.action_join), + color = ElementTheme.colors.textSuccessPrimary, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 4.dp, end = 12.dp) + ) } else { Spacer(modifier = Modifier.width(24.dp)) } @@ -229,8 +279,8 @@ private fun RoomDirectorySearchTopBar( @PreviewsDayNight @Composable -fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectorySearchState) = ElementPreview { - RoomDirectorySearchView( +fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectoryState) = ElementPreview { + RoomDirectoryView( state = state, onBackPressed = {}, ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/datasource/RoomDirectorySearchDataSource.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/datasource/RoomDirectoryDataSource.kt similarity index 75% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/datasource/RoomDirectorySearchDataSource.kt rename to features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/datasource/RoomDirectoryDataSource.kt index 6780c97017..2441c694ee 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/datasource/RoomDirectorySearchDataSource.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/datasource/RoomDirectoryDataSource.kt @@ -14,24 +14,23 @@ * limitations under the License. */ -package io.element.android.features.roomdirectory.impl.search.datasource +package io.element.android.features.roomdirectory.impl.root.datasource -import io.element.android.features.roomdirectory.impl.search.model.RoomDirectorySearchResult +import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryRoomSummary 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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOf import javax.inject.Inject -class RoomDirectorySearchDataSource @Inject constructor( - -) { - - private val _searchResults = MutableStateFlow>(persistentListOf()) +class RoomDirectoryDataSource @Inject constructor() { + private val _searchResults = MutableStateFlow>(persistentListOf()) suspend fun updateSearchQuery(searchQuery: String) { //TODO branch to matrix sdk @@ -39,7 +38,9 @@ class RoomDirectorySearchDataSource @Inject constructor( _searchResults.value = persistentListOf() } else { delay(100) - emitFakeResults() + _searchResults.value = all.value.filter { + it.name.contains(searchQuery) + }.toImmutableList() } } @@ -47,9 +48,10 @@ class RoomDirectorySearchDataSource @Inject constructor( //TODO branch to matrix sdk } - private fun emitFakeResults() { - _searchResults.value = persistentListOf( - RoomDirectorySearchResult( + + val all: StateFlow> = MutableStateFlow( + persistentListOf( + RoomDirectoryRoomSummary( roomId = RoomId("!exa:matrix.org"), name = "Element X Android", description = "Element X is a secure, private and decentralized messenger.", @@ -61,7 +63,7 @@ class RoomDirectorySearchDataSource @Inject constructor( ), canBeJoined = true, ), - RoomDirectorySearchResult( + RoomDirectoryRoomSummary( roomId = RoomId("!exi:matrix.org"), name = "Element X iOS", description = "Element X is a secure, private and decentralized messenger.", @@ -74,7 +76,6 @@ class RoomDirectorySearchDataSource @Inject constructor( canBeJoined = false, ) ) - } - - val searchResults: StateFlow> = _searchResults + ) + val searchResults: StateFlow> = _searchResults } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/model/RoomDirectorySearchResult.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryRoomSummary.kt similarity index 89% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/model/RoomDirectorySearchResult.kt rename to features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryRoomSummary.kt index ef531e716d..7aedc1305b 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/search/model/RoomDirectorySearchResult.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryRoomSummary.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.element.android.features.roomdirectory.impl.search.model +package io.element.android.features.roomdirectory.impl.root.model import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.RoomId -data class RoomDirectorySearchResult( +data class RoomDirectoryRoomSummary( val roomId: RoomId, val name: String, val description: String, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt index e8c68cebf0..9af30fbdcd 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt @@ -208,7 +208,7 @@ private fun RoomDirectorySearchButton( modifier: Modifier = Modifier ) { Button( - text = "Room directory", + text = "Browse all rooms", leadingIcon = IconSource.Vector(CompoundIcons.ListBulleted()), onClick = onClick, modifier = modifier, From 01e6b46323290db0f378b47b6dfda734c941213e Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 21 Mar 2024 19:14:05 +0100 Subject: [PATCH 05/21] Room Directory : start exposing the matrix apis --- .../libraries/matrix/api/MatrixClient.kt | 2 + .../api/roomdirectory/RoomDescription.kt | 36 +++++++ .../api/roomdirectory/RoomDirectorySearch.kt | 26 +++++ .../api/roomdirectory/RoomDirectoryService.kt | 21 ++++ .../libraries/matrix/impl/RustMatrixClient.kt | 11 +++ .../roomdirectory/RoomDescriptionMapper.kt | 42 ++++++++ .../RoomDirectorySearchExtension.kt | 45 +++++++++ .../RoomDirectorySearchProcessor.kt | 97 +++++++++++++++++++ .../roomdirectory/RustRoomDirectorySearch.kt | 65 +++++++++++++ .../roomdirectory/RustRoomDirectoryService.kt | 35 +++++++ .../libraries/matrix/test/FakeMatrixClient.kt | 5 + .../roomdirectory/FakeRoomDirectoryService.kt | 26 +++++ 12 files changed, 411 insertions(+) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDescription.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectorySearch.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchExtension.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectorySearch.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 955c8d72fa..43ca8f6687 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -29,6 +29,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.sync.SyncService import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults @@ -64,6 +65,7 @@ interface MatrixClient : Closeable { fun notificationService(): NotificationService fun notificationSettingsService(): NotificationSettingsService fun encryptionService(): EncryptionService + fun roomDirectoryService(): RoomDirectoryService suspend fun getCacheSize(): Long /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDescription.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDescription.kt new file mode 100644 index 0000000000..dddd556bbe --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDescription.kt @@ -0,0 +1,36 @@ +/* + * 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.api.roomdirectory + +import io.element.android.libraries.matrix.api.core.RoomId + +data class RoomDescription( + val roomId: RoomId, + val name: String?, + val topic: String?, + val alias: String?, + val avatarUrl: String?, + val joinRule: JoinRule, + val isWorldReadable: Boolean, + val joinedMembers: Long +) { + enum class JoinRule { + PUBLIC, + KNOCK, + UNKNOWN + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectorySearch.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectorySearch.kt new file mode 100644 index 0000000000..1902c7aa3a --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectorySearch.kt @@ -0,0 +1,26 @@ +/* + * 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.api.roomdirectory + +import kotlinx.coroutines.flow.SharedFlow + +interface RoomDirectorySearch { + suspend fun updateQuery(query: String?, batchSize: Int) + suspend fun loadMore() + suspend fun hasMoreToLoad(): Boolean + val results: SharedFlow> +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt new file mode 100644 index 0000000000..ebcff30671 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt @@ -0,0 +1,21 @@ +/* + * 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.api.roomdirectory + +interface RoomDirectoryService { + fun search(): RoomDirectorySearch +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 31e439bbec..280b56724b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -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 @@ -150,6 +152,13 @@ class RustMatrixClient( sessionCoroutineScope = sessionCoroutineScope, dispatchers = dispatchers, ) + + private val roomDirectoryService = RustRoomDirectoryService( + client = client, + sessionCoroutineScope = sessionCoroutineScope, + sessionDispatcher = sessionDispatcher, + ) + private val sessionDirectoryNameProvider = SessionDirectoryNameProvider() private val isLoggingOut = AtomicBoolean(false) @@ -428,6 +437,8 @@ class RustMatrixClient( override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService + override fun roomDirectoryService(): RoomDirectoryService = roomDirectoryService + override fun close() { sessionCoroutineScope.cancel() clientDelegateTaskHandle?.cancelAndDestroy() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt new file mode 100644 index 0000000000..52b8e1ba98 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt @@ -0,0 +1,42 @@ +/* + * 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, + joinedMembers = roomDescription.joinedMembers.toLong(), + ) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchExtension.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchExtension.kt new file mode 100644 index 0000000000..28e82f4e40 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchExtension.kt @@ -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> = + callbackFlow { + val listener = object : RoomDirectorySearchEntriesListener { + override fun onUpdate(roomEntriesUpdate: List) { + trySendBlocking(roomEntriesUpdate) + } + } + val result = results(listener) + awaitClose { + result.cancelAndDestroy() + } + }.catch { + Timber.d(it, "timelineDiffFlow() failed") + }.buffer(Channel.UNLIMITED) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt new file mode 100644 index 0000000000..bfa996ac0d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt @@ -0,0 +1,97 @@ +/* + * 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>, + private val coroutineContext: CoroutineContext, + private val roomDescriptionMapper: RoomDescriptionMapper, +) { + + private val mutex = Mutex() + + suspend fun postUpdates(updates: List) { + updateRoomDescriptions { + Timber.v("Update room descriptions from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}") + updates.forEach { update -> + applyUpdate(update) + } + } + } + + private suspend fun MutableList.applyUpdate(update: RoomDirectorySearchEntryUpdate) { + when (update) { + is RoomDirectorySearchEntryUpdate.Append -> { + val roomSummaries = update.values.map(roomDescriptionMapper::map) + addAll(roomSummaries) + } + is RoomDirectorySearchEntryUpdate.PushBack -> { + val roomSummary = roomDescriptionMapper.map(update.value) + add(roomSummary) + } + is RoomDirectorySearchEntryUpdate.PushFront -> { + val roomSummary = roomDescriptionMapper.map(update.value) + add(0, roomSummary) + } + is RoomDirectorySearchEntryUpdate.Set -> { + val roomSummary = roomDescriptionMapper.map(update.value) + this[update.index.toInt()] = roomSummary + } + is RoomDirectorySearchEntryUpdate.Insert -> { + val roomSummary = roomDescriptionMapper.map(update.value) + add(update.index.toInt(), roomSummary) + } + 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.() -> Unit) = withContext(coroutineContext) { + mutex.withLock { + val current = roomDescriptions.replayCache.lastOrNull() + val mutableRoomSummaries = current.orEmpty().toMutableList() + block(mutableRoomSummaries) + roomDescriptions.emit(mutableRoomSummaries) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectorySearch.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectorySearch.kt new file mode 100644 index 0000000000..448a229538 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectorySearch.kt @@ -0,0 +1,65 @@ +/* + * 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.RoomDirectorySearch +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.matrix.rustcomponents.sdk.RoomDirectorySearch as InnerRoomDirectorySearch + +class RustRoomDirectorySearch( + private val inner: InnerRoomDirectorySearch, + private val sessionCoroutineScope: CoroutineScope, + private val sessionDispatcher: CoroutineDispatcher, +) : RoomDirectorySearch { + + private val _results: MutableStateFlow> = + MutableStateFlow(emptyList()) + + private val processor = RoomDirectorySearchProcessor(_results, sessionDispatcher, RoomDescriptionMapper()) + + init { + sessionCoroutineScope.launch(sessionDispatcher) { + inner + .resultsFlow() + .onEach { updates -> + processor.postUpdates(updates) + } + .launchIn(this) + } + } + + override suspend fun updateQuery(query: String?, batchSize: Int) { + inner.search(query, batchSize.toUInt()) + } + + override suspend fun loadMore() { + inner.nextPage() + } + + override suspend fun hasMoreToLoad(): Boolean { + return !inner.isAtLastPage() + } + + override val results: SharedFlow> = _results +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt new file mode 100644 index 0000000000..9644d5db7a --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt @@ -0,0 +1,35 @@ +/* + * 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.RoomDirectorySearch +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 sessionCoroutineScope: CoroutineScope, + private val sessionDispatcher: CoroutineDispatcher, +) : RoomDirectoryService { + + override fun search(): RoomDirectorySearch { + return RustRoomDirectorySearch(client.roomDirectorySearch(), sessionCoroutineScope, sessionDispatcher) + } + +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index d7c3f71e64..1ac68a5fa5 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -30,6 +30,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.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser @@ -39,6 +40,7 @@ import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.pushers.FakePushersService +import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService @@ -65,6 +67,7 @@ class FakeMatrixClient( private val notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), private val syncService: FakeSyncService = FakeSyncService(), private val encryptionService: FakeEncryptionService = FakeEncryptionService(), + private val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(), private val accountManagementUrlString: Result = Result.success(null), ) : MatrixClient { var setDisplayNameCalled: Boolean = false @@ -126,6 +129,8 @@ class FakeMatrixClient( override fun syncService() = syncService + override fun roomDirectoryService() = roomDirectoryService + override suspend fun getCacheSize(): Long { return 0 } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt new file mode 100644 index 0000000000..f4b99f09a1 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt @@ -0,0 +1,26 @@ +/* + * 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.test.roomdirectory + +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectorySearch +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService + +class FakeRoomDirectoryService : RoomDirectoryService { + override fun search(): RoomDirectorySearch { + TODO("Not yet implemented") + } +} From 4c5ae6ae4bcd9afe658c81ecbbeb4b8467022efd Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 25 Mar 2024 11:17:19 +0100 Subject: [PATCH 06/21] RoomDirectory : continue implementing the search --- .../impl/root/RoomDirectoryPresenter.kt | 71 ++++--- .../impl/root/RoomDirectoryState.kt | 12 +- .../impl/root/RoomDirectoryStateProvider.kt | 22 +-- .../impl/root/RoomDirectoryView.kt | 179 ++++++++---------- .../datasource/RoomDirectoryDataSource.kt | 81 -------- ...omSummary.kt => RoomDescriptionUiModel.kt} | 19 +- .../components/avatar/AvatarSize.kt | 2 +- ...irectorySearch.kt => RoomDirectoryList.kt} | 8 +- .../api/roomdirectory/RoomDirectoryService.kt | 2 +- .../matrix/impl/di/SessionMatrixModule.kt | 6 + ...torySearch.kt => RustRoomDirectoryList.kt} | 25 +-- .../roomdirectory/RustRoomDirectoryService.kt | 6 +- .../roomdirectory/FakeRoomDirectoryService.kt | 4 +- 13 files changed, 190 insertions(+), 247 deletions(-) delete mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/datasource/RoomDirectoryDataSource.kt rename features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/{RoomDirectoryRoomSummary.kt => RoomDescriptionUiModel.kt} (60%) rename libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/{RoomDirectorySearch.kt => RoomDirectoryList.kt} (79%) rename libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/{RustRoomDirectorySearch.kt => RustRoomDirectoryList.kt} (71%) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt index 5ab2dfe6eb..78300287bf 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt @@ -21,73 +21,98 @@ import androidx.compose.runtime.LaunchedEffect 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.datasource.RoomDirectoryDataSource +import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel +import io.element.android.features.roomdirectory.impl.root.model.toUiModel import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.RoomId -import kotlinx.coroutines.CoroutineScope +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject class RoomDirectoryPresenter @Inject constructor( private val client: MatrixClient, - private val dataSource: RoomDirectoryDataSource, + private val roomDirectoryService: RoomDirectoryService, ) : Presenter { + private val roomDirectoryList = roomDirectoryService.createRoomDirectoryList() + @Composable override fun present(): RoomDirectoryState { var searchQuery by rememberSaveable { mutableStateOf("") } - var isSearchActive by rememberSaveable { - mutableStateOf(false) + val allRooms by roomDirectoryList.collectItemsAsState() + val hasMoreToLoad by produceState(initialValue = true, allRooms) { + value = roomDirectoryList.hasMoreToLoad() } - - val roomSummaries by dataSource.all.collectAsState() - val coroutineScope = rememberCoroutineScope() - LaunchedEffect(searchQuery) { - dataSource.updateSearchQuery(searchQuery) + roomDirectoryList.filter(searchQuery, 20) } - fun handleEvents(event: RoomDirectoryEvents) { when (event) { is RoomDirectoryEvents.JoinRoom -> { - coroutineScope.joinRoom(event.roomId) + //coroutineScope.joinRoom(event.roomId) } RoomDirectoryEvents.LoadMore -> { coroutineScope.launch { - dataSource.loadMore() + roomDirectoryList.loadMore() } } is RoomDirectoryEvents.Search -> { searchQuery = event.query } is RoomDirectoryEvents.SearchActiveChange -> { - isSearchActive = event.isActive - if (!isSearchActive) { - searchQuery = "" - } + } } } return RoomDirectoryState( query = searchQuery, - isSearchActive = isSearchActive, - roomSummaries = roomSummaries, - searchResults = SearchBarResultState.Initial(), + roomDescriptions = allRooms, + displayLoadMoreIndicator = hasMoreToLoad, eventSink = ::handleEvents ) } - private fun CoroutineScope.joinRoom(roomId: RoomId) = launch { - client.getRoom(roomId)?.join() + @Composable + private fun searchResults( + filteredRooms: ImmutableList, + hasMoreToLoad: Boolean, + isSearchActive: Boolean, + ): SearchBarResultState> { + return if (!isSearchActive) { + SearchBarResultState.Initial() + } else { + if (filteredRooms.isEmpty() && !hasMoreToLoad) { + SearchBarResultState.NoResultsFound() + } else { + SearchBarResultState.Results(filteredRooms) + } + } } + + @Composable + private fun RoomDirectoryList.collectItemsAsState() = remember { + items.map { list -> + list + .map { roomDescription -> roomDescription.toUiModel() } + .toImmutableList() + }.flowOn(Dispatchers.Default) + }.collectAsState(persistentListOf()) } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt index 3abf725a24..1afd53f146 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt @@ -16,14 +16,14 @@ package io.element.android.features.roomdirectory.impl.root -import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryRoomSummary -import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel import kotlinx.collections.immutable.ImmutableList data class RoomDirectoryState( val query: String, - val roomSummaries: ImmutableList, - val searchResults: SearchBarResultState>, - val isSearchActive: Boolean, + val roomDescriptions: ImmutableList, + val displayLoadMoreIndicator: Boolean, val eventSink: (RoomDirectoryEvents) -> Unit -) +) { + val displayEmptyState = roomDescriptions.isEmpty() && !displayLoadMoreIndicator +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt index a9d1f2287f..30acb14dba 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt @@ -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.RoomDirectoryRoomSummary +import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.theme.components.SearchBarResultState @@ -31,8 +31,8 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider = persistentListOf(), - searchResults: SearchBarResultState> = SearchBarResultState.Initial(), + displayLoadMoreIndicator: Boolean = false, + roomDescriptions: ImmutableList = persistentListOf(), + searchResults: SearchBarResultState> = SearchBarResultState.Initial(), ) = RoomDirectoryState( query = query, - isSearchActive = isSearchActive, - roomSummaries = roomSummaries, - searchResults = searchResults, + roomDescriptions = roomDescriptions, + displayLoadMoreIndicator = displayLoadMoreIndicator, eventSink = {}, ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index b79e4cfd20..0bff02cdc2 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -17,37 +17,33 @@ package io.element.android.features.roomdirectory.impl.root import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow 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.RoomDirectoryRoomSummary +import io.element.android.features.roomdirectory.impl.root.model.RoomDescriptionUiModel import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview @@ -56,7 +52,6 @@ import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.SearchBar import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -109,71 +104,107 @@ private fun RoomDirectoryTopBar( ) } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun RoomDirectoryContent( state: RoomDirectoryState, onResultClicked: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - SearchBar( + Column(modifier = modifier) { + SearchTextField( query = state.query, - onQueryChange = { query -> - state.eventSink(RoomDirectoryEvents.Search(query)) - }, - active = state.isSearchActive, - onActiveChange = { - state.eventSink(RoomDirectoryEvents.SearchActiveChange(it)) - }, - resultState = state.searchResults, - placeHolderTitle = stringResource(id = CommonStrings.action_search), - ) { results -> - RoomDirectoryRoomList( - rooms = results, - onResultClicked = onResultClicked, - ) - } - if (!state.isSearchActive) { - RoomDirectoryRoomList( - rooms = state.roomSummaries, - onResultClicked = onResultClicked, - ) - } + onQueryChange = { state.eventSink(RoomDirectoryEvents.Search(it)) }, + placeholder = stringResource(id = CommonStrings.action_search), + modifier = Modifier.fillMaxWidth(), + ) + RoomDirectoryRoomList( + roomDescriptions = state.roomDescriptions, + onResultClicked = onResultClicked, + ) } } @Composable private fun RoomDirectoryRoomList( - rooms: ImmutableList, + roomDescriptions: ImmutableList, onResultClicked: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( modifier = modifier, ) { - items(rooms) { room -> + items(roomDescriptions) { roomDescription -> RoomDirectoryRoomRow( - room = room, + roomDescription = roomDescription, onClick = onResultClicked, ) } } } +@Composable +private fun SearchTextField( + query: String, + onQueryChange: (String) -> Unit, + placeholder: String, + modifier: Modifier = Modifier, + colors: TextFieldColors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + unfocusedPlaceholderColor = ElementTheme.colors.textPlaceholder, + focusedPlaceholderColor = ElementTheme.colors.textPlaceholder, + focusedTextColor = ElementTheme.colors.textPrimary, + unfocusedTextColor = ElementTheme.colors.textPrimary, + focusedIndicatorColor = ElementTheme.colors.borderInteractiveSecondary, + unfocusedIndicatorColor = ElementTheme.colors.borderInteractiveSecondary, + ), +) { + val focusManager = LocalFocusManager.current + TextField( + modifier = modifier, + textStyle = ElementTheme.typography.fontBodyLgRegular, + singleLine = true, + value = query, + onValueChange = onQueryChange, + keyboardActions = KeyboardActions( + onSearch = { + focusManager.clearFocus() + } + ), + colors = colors, + placeholder = { Text(placeholder) }, + trailingIcon = { + if (query.isNotEmpty()) { + IconButton( + onClick = { + onQueryChange("") + } + ) { + Icon( + imageVector = CompoundIcons.Close(), + contentDescription = stringResource(CommonStrings.action_clear), + ) + } + } else { + Icon( + imageVector = CompoundIcons.Search(), + contentDescription = stringResource(CommonStrings.action_search), + ) + } + }, + ) +} + @Composable private fun RoomDirectoryRoomRow( - room: RoomDirectoryRoomSummary, + roomDescription: RoomDescriptionUiModel, onClick: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier .fillMaxWidth() - .clickable { onClick(room.roomId) } + .clickable { onClick(roomDescription.roomId) } .padding( top = 12.dp, bottom = 12.dp, @@ -182,7 +213,7 @@ private fun RoomDirectoryRoomRow( .height(IntrinsicSize.Min), ) { Avatar( - avatarData = room.avatarData, + avatarData = roomDescription.avatarData, modifier = Modifier.align(Alignment.CenterVertically) ) Column( @@ -191,21 +222,21 @@ private fun RoomDirectoryRoomRow( .padding(start = 16.dp) ) { Text( - text = room.name, + text = roomDescription.name, maxLines = 1, style = ElementTheme.typography.fontBodyLgRegular, color = ElementTheme.colors.textPrimary, overflow = TextOverflow.Ellipsis, ) Text( - text = room.description, + text = roomDescription.description, maxLines = 1, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textSecondary, overflow = TextOverflow.Ellipsis, ) } - if (room.canBeJoined) { + if (roomDescription.canBeJoined) { Text( text = stringResource(id = CommonStrings.action_join), color = ElementTheme.colors.textSuccessPrimary, @@ -219,64 +250,6 @@ private fun RoomDirectoryRoomRow( } } -@Composable -@OptIn(ExperimentalMaterial3Api::class) -private fun RoomDirectorySearchTopBar( - query: String, - onQueryChanged: (String) -> Unit, - onBackPressed: () -> Unit, - modifier: Modifier = Modifier, -) { - val borderColor = ElementTheme.colors.borderInteractivePrimary - val borderStroke = 1.dp - TopAppBar( - modifier = modifier.drawBehind { - drawLine( - color = borderColor, - start = Offset(0f, size.height), - end = Offset(size.width, size.height), - strokeWidth = borderStroke.value - ) - }, - title = { - val focusRequester = FocusRequester() - TextField( - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - value = query, - singleLine = true, - onValueChange = onQueryChanged, - colors = TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - disabledContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent, - ), - trailingIcon = { - if (query.isNotEmpty()) { - IconButton(onClick = { - onQueryChanged("") - }) { - Icon( - imageVector = CompoundIcons.Close(), - contentDescription = stringResource(CommonStrings.action_cancel), - ) - } - } - } - ) - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } - }, - navigationIcon = { BackButton(onClick = onBackPressed) }, - ) -} - @PreviewsDayNight @Composable fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectoryState) = ElementPreview { diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/datasource/RoomDirectoryDataSource.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/datasource/RoomDirectoryDataSource.kt deleted file mode 100644 index 2441c694ee..0000000000 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/datasource/RoomDirectoryDataSource.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.datasource - -import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryRoomSummary -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 kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flowOf -import javax.inject.Inject - -class RoomDirectoryDataSource @Inject constructor() { - private val _searchResults = MutableStateFlow>(persistentListOf()) - - suspend fun updateSearchQuery(searchQuery: String) { - //TODO branch to matrix sdk - if (searchQuery.isEmpty()) { - _searchResults.value = persistentListOf() - } else { - delay(100) - _searchResults.value = all.value.filter { - it.name.contains(searchQuery) - }.toImmutableList() - } - } - - suspend fun loadMore() { - //TODO branch to matrix sdk - } - - - val all: StateFlow> = MutableStateFlow( - persistentListOf( - RoomDirectoryRoomSummary( - roomId = RoomId("!exa:matrix.org"), - name = "Element X Android", - description = "Element X is a secure, private and decentralized messenger.", - avatarData = AvatarData( - id = "!exa:matrix.org", - name = "Element X Android", - url = null, - size = AvatarSize.RoomDirectorySearchItem - ), - canBeJoined = true, - ), - RoomDirectoryRoomSummary( - roomId = RoomId("!exi:matrix.org"), - name = "Element X iOS", - description = "Element X is a secure, private and decentralized messenger.", - avatarData = AvatarData( - id = "!exi:matrix.org", - name = "Element X iOS", - url = null, - size = AvatarSize.RoomDirectorySearchItem - ), - canBeJoined = false, - ) - ) - ) - val searchResults: StateFlow> = _searchResults -} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryRoomSummary.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescriptionUiModel.kt similarity index 60% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryRoomSummary.kt rename to features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescriptionUiModel.kt index 7aedc1305b..c7424eebf0 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryRoomSummary.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescriptionUiModel.kt @@ -17,12 +17,29 @@ package io.element.android.features.roomdirectory.impl.root.model 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 -data class RoomDirectoryRoomSummary( +data class RoomDescriptionUiModel( val roomId: RoomId, val name: String, val description: String, val avatarData: AvatarData, val canBeJoined: Boolean, ) + +fun RoomDescription.toUiModel(): RoomDescriptionUiModel { + return RoomDescriptionUiModel( + roomId = roomId, + name = name ?: "", + description = topic ?: "", + avatarData = AvatarData( + id = roomId.value, + name = name ?: "", + url = avatarUrl, + size = AvatarSize.RoomDirectoryItem, + ), + canBeJoined = joinRule == RoomDescription.JoinRule.PUBLIC, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 382aac5468..2dc6c8875f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -53,5 +53,5 @@ enum class AvatarSize(val dp: Dp) { CustomRoomNotificationSetting(36.dp), - RoomDirectorySearchItem(36.dp), + RoomDirectoryItem(36.dp), } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectorySearch.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt similarity index 79% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectorySearch.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt index 1902c7aa3a..ce50125b65 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectorySearch.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt @@ -16,11 +16,11 @@ package io.element.android.libraries.matrix.api.roomdirectory -import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.Flow -interface RoomDirectorySearch { - suspend fun updateQuery(query: String?, batchSize: Int) +interface RoomDirectoryList { + suspend fun filter(filter: String?, batchSize: Int) suspend fun loadMore() suspend fun hasMoreToLoad(): Boolean - val results: SharedFlow> + val items: Flow> } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt index ebcff30671..fe9beb27d6 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt @@ -17,5 +17,5 @@ package io.element.android.libraries.matrix.api.roomdirectory interface RoomDirectoryService { - fun search(): RoomDirectorySearch + fun createRoomDirectoryList(): RoomDirectoryList } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index 17ea8ee444..bf5c4c601f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -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() + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectorySearch.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt similarity index 71% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectorySearch.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt index 448a229538..1dbd62317c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectorySearch.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt @@ -17,26 +17,28 @@ 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.RoomDirectorySearch +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.debounce 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 -class RustRoomDirectorySearch( +class RustRoomDirectoryList( private val inner: InnerRoomDirectorySearch, private val sessionCoroutineScope: CoroutineScope, private val sessionDispatcher: CoroutineDispatcher, -) : RoomDirectorySearch { +) : RoomDirectoryList { - private val _results: MutableStateFlow> = - MutableStateFlow(emptyList()) + private val _items = MutableSharedFlow>() - private val processor = RoomDirectorySearchProcessor(_results, sessionDispatcher, RoomDescriptionMapper()) + private val processor = RoomDirectorySearchProcessor(_items, sessionDispatcher, RoomDescriptionMapper()) init { sessionCoroutineScope.launch(sessionDispatcher) { @@ -49,8 +51,8 @@ class RustRoomDirectorySearch( } } - override suspend fun updateQuery(query: String?, batchSize: Int) { - inner.search(query, batchSize.toUInt()) + override suspend fun filter(filter: String?, batchSize: Int) { + inner.search(filter, batchSize.toUInt()) } override suspend fun loadMore() { @@ -61,5 +63,6 @@ class RustRoomDirectorySearch( return !inner.isAtLastPage() } - override val results: SharedFlow> = _results + @OptIn(FlowPreview::class) + override val items: Flow> = _items.debounce(200.milliseconds) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt index 9644d5db7a..7022b2e21b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt @@ -16,7 +16,7 @@ package io.element.android.libraries.matrix.impl.roomdirectory -import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectorySearch +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 @@ -28,8 +28,8 @@ class RustRoomDirectoryService( private val sessionDispatcher: CoroutineDispatcher, ) : RoomDirectoryService { - override fun search(): RoomDirectorySearch { - return RustRoomDirectorySearch(client.roomDirectorySearch(), sessionCoroutineScope, sessionDispatcher) + override fun createRoomDirectoryList(): RoomDirectoryList { + return RustRoomDirectoryList(client.roomDirectorySearch(), sessionCoroutineScope, sessionDispatcher) } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt index f4b99f09a1..9281a41bc8 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt @@ -16,11 +16,11 @@ package io.element.android.libraries.matrix.test.roomdirectory -import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectorySearch +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService class FakeRoomDirectoryService : RoomDirectoryService { - override fun search(): RoomDirectorySearch { + override fun createRoomDirectoryList(): RoomDirectoryList { TODO("Not yet implemented") } } From e4c711842838ee50181bbb88b8c074349c3a609b Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 25 Mar 2024 18:21:03 +0100 Subject: [PATCH 07/21] Room directory : fix pagination and add empty state. --- .../impl/root/RoomDirectoryEvents.kt | 1 - .../impl/root/RoomDirectoryPresenter.kt | 23 ------ .../impl/root/RoomDirectoryView.kt | 75 ++++++++++++++----- .../api/roomdirectory/RoomDirectoryList.kt | 4 +- .../RoomDirectorySearchProcessor.kt | 18 ++--- .../roomdirectory/RustRoomDirectoryList.kt | 30 ++++++-- 6 files changed, 91 insertions(+), 60 deletions(-) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt index 267c889f61..771a8ceb6c 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt @@ -22,5 +22,4 @@ sealed interface RoomDirectoryEvents { data class Search(val query: String) : RoomDirectoryEvents data object LoadMore : RoomDirectoryEvents data class JoinRoom(val roomId: RoomId) : RoomDirectoryEvents - data class SearchActiveChange(val isActive: Boolean) : RoomDirectoryEvents } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt index 78300287bf..70d65bfb37 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt @@ -26,14 +26,11 @@ 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.RoomDescriptionUiModel import io.element.android.features.roomdirectory.impl.root.model.toUiModel import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers @@ -76,9 +73,6 @@ class RoomDirectoryPresenter @Inject constructor( is RoomDirectoryEvents.Search -> { searchQuery = event.query } - is RoomDirectoryEvents.SearchActiveChange -> { - - } } } @@ -90,23 +84,6 @@ class RoomDirectoryPresenter @Inject constructor( ) } - @Composable - private fun searchResults( - filteredRooms: ImmutableList, - hasMoreToLoad: Boolean, - isSearchActive: Boolean, - ): SearchBarResultState> { - return if (!isSearchActive) { - SearchBarResultState.Initial() - } else { - if (filteredRooms.isEmpty() && !hasMoreToLoad) { - SearchBarResultState.NoResultsFound() - } else { - SearchBarResultState.Results(filteredRooms) - } - } - } - @Composable private fun RoomDirectoryList.collectItemsAsState() = remember { items.map { list -> diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index 0bff02cdc2..3bc7a0886c 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -17,6 +17,7 @@ package io.element.android.features.roomdirectory.impl.root import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row @@ -26,6 +27,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.KeyboardActions @@ -33,6 +35,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -49,6 +52,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Scaffold @@ -77,8 +81,8 @@ fun RoomDirectoryView( state.eventSink(RoomDirectoryEvents.JoinRoom(roomId)) }, modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) } ) @@ -119,7 +123,10 @@ private fun RoomDirectoryContent( ) RoomDirectoryRoomList( roomDescriptions = state.roomDescriptions, + displayLoadMoreIndicator = state.displayLoadMoreIndicator, + displayEmptyState = state.displayEmptyState, onResultClicked = onResultClicked, + onReachedLoadMore = { state.eventSink(RoomDirectoryEvents.LoadMore) }, ) } } @@ -127,18 +134,52 @@ private fun RoomDirectoryContent( @Composable private fun RoomDirectoryRoomList( roomDescriptions: ImmutableList, + displayLoadMoreIndicator: Boolean, + displayEmptyState: Boolean, onResultClicked: (RoomId) -> Unit, + onReachedLoadMore: () -> Unit, modifier: Modifier = Modifier, ) { - LazyColumn( - modifier = modifier, - ) { + LazyColumn(modifier = modifier) { items(roomDescriptions) { roomDescription -> RoomDirectoryRoomRow( roomDescription = roomDescription, onClick = onResultClicked, ) } + if (displayEmptyState) { + item { + Text( + text = stringResource(id = CommonStrings.common_no_results), + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPlaceholder, + modifier = Modifier.padding(16.dp) + ) + } + } + if (displayLoadMoreIndicator) { + item { + LoadMoreIndicator(modifier = Modifier.fillMaxWidth()) + LaunchedEffect(Unit) { + onReachedLoadMore() + } + } + } + } +} + +@Composable +private fun LoadMoreIndicator(modifier: Modifier = Modifier) { + Box( + modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(8.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + strokeWidth = 2.dp, + ) } } @@ -203,14 +244,14 @@ private fun RoomDirectoryRoomRow( ) { Row( modifier = modifier - .fillMaxWidth() - .clickable { onClick(roomDescription.roomId) } - .padding( - top = 12.dp, - bottom = 12.dp, - start = 16.dp, - ) - .height(IntrinsicSize.Min), + .fillMaxWidth() + .clickable { onClick(roomDescription.roomId) } + .padding( + top = 12.dp, + bottom = 12.dp, + start = 16.dp, + ) + .height(IntrinsicSize.Min), ) { Avatar( avatarData = roomDescription.avatarData, @@ -218,8 +259,8 @@ private fun RoomDirectoryRoomRow( ) Column( modifier = Modifier - .weight(1f) - .padding(start = 16.dp) + .weight(1f) + .padding(start = 16.dp) ) { Text( text = roomDescription.name, @@ -241,8 +282,8 @@ private fun RoomDirectoryRoomRow( text = stringResource(id = CommonStrings.action_join), color = ElementTheme.colors.textSuccessPrimary, modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 4.dp, end = 12.dp) + .align(Alignment.CenterVertically) + .padding(start = 4.dp, end = 12.dp) ) } else { Spacer(modifier = Modifier.width(24.dp)) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt index ce50125b65..f232983620 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt @@ -19,8 +19,8 @@ package io.element.android.libraries.matrix.api.roomdirectory import kotlinx.coroutines.flow.Flow interface RoomDirectoryList { - suspend fun filter(filter: String?, batchSize: Int) - suspend fun loadMore() + suspend fun filter(filter: String?, batchSize: Int): Result + suspend fun loadMore(): Result suspend fun hasMoreToLoad(): Boolean val items: Flow> } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt index bfa996ac0d..f060635cdf 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt @@ -42,27 +42,27 @@ class RoomDirectorySearchProcessor( } } - private suspend fun MutableList.applyUpdate(update: RoomDirectorySearchEntryUpdate) { + private fun MutableList.applyUpdate(update: RoomDirectorySearchEntryUpdate) { when (update) { is RoomDirectorySearchEntryUpdate.Append -> { val roomSummaries = update.values.map(roomDescriptionMapper::map) addAll(roomSummaries) } is RoomDirectorySearchEntryUpdate.PushBack -> { - val roomSummary = roomDescriptionMapper.map(update.value) - add(roomSummary) + val roomDescription = roomDescriptionMapper.map(update.value) + add(roomDescription) } is RoomDirectorySearchEntryUpdate.PushFront -> { - val roomSummary = roomDescriptionMapper.map(update.value) - add(0, roomSummary) + val roomDescription = roomDescriptionMapper.map(update.value) + add(0, roomDescription) } is RoomDirectorySearchEntryUpdate.Set -> { - val roomSummary = roomDescriptionMapper.map(update.value) - this[update.index.toInt()] = roomSummary + val roomDescription = roomDescriptionMapper.map(update.value) + this[update.index.toInt()] = roomDescription } is RoomDirectorySearchEntryUpdate.Insert -> { - val roomSummary = roomDescriptionMapper.map(update.value) - add(update.index.toInt(), roomSummary) + val roomDescription = roomDescriptionMapper.map(update.value) + add(update.index.toInt(), roomDescription) } is RoomDirectorySearchEntryUpdate.Remove -> { removeAt(update.index.toInt()) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt index 1dbd62317c..7b7296db8e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt @@ -18,6 +18,7 @@ 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 @@ -32,12 +33,11 @@ import org.matrix.rustcomponents.sdk.RoomDirectorySearch as InnerRoomDirectorySe class RustRoomDirectoryList( private val inner: InnerRoomDirectorySearch, - private val sessionCoroutineScope: CoroutineScope, - private val sessionDispatcher: CoroutineDispatcher, + sessionCoroutineScope: CoroutineScope, + sessionDispatcher: CoroutineDispatcher, ) : RoomDirectoryList { - private val _items = MutableSharedFlow>() - + private val _items = MutableSharedFlow>(replay = 1) private val processor = RoomDirectorySearchProcessor(_items, sessionDispatcher, RoomDescriptionMapper()) init { @@ -51,12 +51,26 @@ class RustRoomDirectoryList( } } - override suspend fun filter(filter: String?, batchSize: Int) { - inner.search(filter, batchSize.toUInt()) + override suspend fun filter(filter: String?, batchSize: Int): Result { + return try { + 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() { - inner.nextPage() + override suspend fun loadMore(): Result { + return try { + inner.nextPage() + Result.success(Unit) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Result.failure(e) + } } override suspend fun hasMoreToLoad(): Boolean { From 0c96769e8b29450784bbd668da1604f2570650f0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 25 Mar 2024 20:10:16 +0100 Subject: [PATCH 08/21] Room directory : start branching join event --- .../android/appnav/LoggedInFlowNode.kt | 2 +- .../api/RoomDirectoryEntryPoint.kt | 2 +- .../impl/DefaultRoomDirectoryEntryPoint.kt | 3 +- .../impl/RoomDirectoryFlowNode.kt | 66 ------------------- .../impl/root/RoomDirectoryEvents.kt | 3 - .../impl/root/RoomDirectoryNode.kt | 10 +++ .../impl/root/RoomDirectoryPresenter.kt | 11 ++-- .../impl/root/RoomDirectoryView.kt | 42 ++++++------ 8 files changed, 39 insertions(+), 100 deletions(-) delete mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/RoomDirectoryFlowNode.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 9a96a01d27..82a645b636 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -389,7 +389,7 @@ class LoggedInFlowNode @AssistedInject constructor( NavTarget.RoomDirectorySearch -> { roomDirectoryEntryPoint.nodeBuilder(this, buildContext) .callback(object : RoomDirectoryEntryPoint.Callback { - override fun onRoomJoined(roomId: RoomId) { + override fun onJoinRoom(roomId: RoomId) { coroutineScope.launch { attachRoom(roomId) } } }) diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt index 033ec67b33..a665f9f23c 100644 --- a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt @@ -32,7 +32,7 @@ interface RoomDirectoryEntryPoint : FeatureEntryPoint { } interface Callback : Plugin { - fun onRoomJoined(roomId: RoomId) + fun onJoinRoom(roomId: RoomId) } } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt index ac085b4d03..11b5b1b27d 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt @@ -21,6 +21,7 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint +import io.element.android.features.roomdirectory.impl.root.RoomDirectoryNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope import javax.inject.Inject @@ -39,7 +40,7 @@ class DefaultRoomDirectoryEntryPoint @Inject constructor() : RoomDirectoryEntryP } override fun build(): Node { - return parentNode.createNode(buildContext, plugins) + return parentNode.createNode(buildContext, plugins) } } } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/RoomDirectoryFlowNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/RoomDirectoryFlowNode.kt deleted file mode 100644 index 244e61b3b9..0000000000 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/RoomDirectoryFlowNode.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 - -import android.os.Parcelable -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -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.features.roomdirectory.impl.root.RoomDirectoryNode -import io.element.android.libraries.architecture.BackstackView -import io.element.android.libraries.architecture.BaseFlowNode -import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.SessionScope -import kotlinx.parcelize.Parcelize - -@ContributesNode(SessionScope::class) -class RoomDirectoryFlowNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, -) : BaseFlowNode( - backstack = BackStack( - initialElement = NavTarget.Root, - savedStateMap = buildContext.savedStateMap, - ), - buildContext = buildContext, - plugins = plugins, -) { - - sealed interface NavTarget : Parcelable { - @Parcelize - data object Root : NavTarget - } - - override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { - return when (navTarget) { - NavTarget.Root -> { - createNode(buildContext) - } - } - } - - @Composable - override fun View(modifier: Modifier) { - BackstackView() - } -} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt index 771a8ceb6c..f105a19c53 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt @@ -16,10 +16,7 @@ package io.element.android.features.roomdirectory.impl.root -import io.element.android.libraries.matrix.api.core.RoomId - sealed interface RoomDirectoryEvents { data class Search(val query: String) : RoomDirectoryEvents data object LoadMore : RoomDirectoryEvents - data class JoinRoom(val roomId: RoomId) : RoomDirectoryEvents } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt index 0da712eefe..5c2cbf654c 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt @@ -21,10 +21,13 @@ import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId @ContributesNode(SessionScope::class) class RoomDirectoryNode @AssistedInject constructor( @@ -33,11 +36,18 @@ class RoomDirectoryNode @AssistedInject constructor( private val presenter: RoomDirectoryPresenter, ) : Node(buildContext, plugins = plugins) { + private fun onJoinRoom(roomId: RoomId) { + plugins().forEach { + it.onJoinRoom(roomId) + } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() RoomDirectoryView( state = state, + onJoinRoom = ::onJoinRoom, onBackPressed = ::navigateUp, modifier = modifier ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt index 70d65bfb37..bfd88f4e28 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt @@ -28,20 +28,20 @@ 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.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.MatrixClient 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.Dispatchers import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject class RoomDirectoryPresenter @Inject constructor( - private val client: MatrixClient, - private val roomDirectoryService: RoomDirectoryService, + private val dispatchers: CoroutineDispatchers, + roomDirectoryService: RoomDirectoryService, ) : Presenter { private val roomDirectoryList = roomDirectoryService.createRoomDirectoryList() @@ -62,9 +62,6 @@ class RoomDirectoryPresenter @Inject constructor( } fun handleEvents(event: RoomDirectoryEvents) { when (event) { - is RoomDirectoryEvents.JoinRoom -> { - //coroutineScope.joinRoom(event.roomId) - } RoomDirectoryEvents.LoadMore -> { coroutineScope.launch { roomDirectoryList.loadMore() @@ -90,6 +87,6 @@ class RoomDirectoryPresenter @Inject constructor( list .map { roomDescription -> roomDescription.toUiModel() } .toImmutableList() - }.flowOn(Dispatchers.Default) + }.flowOn(dispatchers.computation) }.collectAsState(persistentListOf()) } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index 3bc7a0886c..fecebf572f 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -67,6 +67,7 @@ import kotlinx.collections.immutable.ImmutableList fun RoomDirectoryView( state: RoomDirectoryState, onBackPressed: () -> Unit, + onJoinRoom: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -77,12 +78,10 @@ fun RoomDirectoryView( content = { padding -> RoomDirectoryContent( state = state, - onResultClicked = { roomId -> - state.eventSink(RoomDirectoryEvents.JoinRoom(roomId)) - }, + onResultClicked = onJoinRoom, modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) } ) @@ -171,10 +170,10 @@ private fun RoomDirectoryRoomList( @Composable private fun LoadMoreIndicator(modifier: Modifier = Modifier) { Box( - modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(8.dp), + modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(8.dp), contentAlignment = Alignment.Center, ) { CircularProgressIndicator( @@ -244,14 +243,14 @@ private fun RoomDirectoryRoomRow( ) { Row( modifier = modifier - .fillMaxWidth() - .clickable { onClick(roomDescription.roomId) } - .padding( - top = 12.dp, - bottom = 12.dp, - start = 16.dp, - ) - .height(IntrinsicSize.Min), + .fillMaxWidth() + .clickable { onClick(roomDescription.roomId) } + .padding( + top = 12.dp, + bottom = 12.dp, + start = 16.dp, + ) + .height(IntrinsicSize.Min), ) { Avatar( avatarData = roomDescription.avatarData, @@ -259,8 +258,8 @@ private fun RoomDirectoryRoomRow( ) Column( modifier = Modifier - .weight(1f) - .padding(start = 16.dp) + .weight(1f) + .padding(start = 16.dp) ) { Text( text = roomDescription.name, @@ -282,8 +281,8 @@ private fun RoomDirectoryRoomRow( text = stringResource(id = CommonStrings.action_join), color = ElementTheme.colors.textSuccessPrimary, modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 4.dp, end = 12.dp) + .align(Alignment.CenterVertically) + .padding(start = 4.dp, end = 12.dp) ) } else { Spacer(modifier = Modifier.width(24.dp)) @@ -296,6 +295,7 @@ private fun RoomDirectoryRoomRow( fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectoryState) = ElementPreview { RoomDirectoryView( state = state, + onJoinRoom = {}, onBackPressed = {}, ) } From 90b2a65c1a8b2e0bce2e3996d38bfd814a119834 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 26 Mar 2024 12:32:15 +0100 Subject: [PATCH 09/21] Room directory : implement simple join room --- .../android/appnav/LoggedInFlowNode.kt | 2 +- .../api/RoomDirectoryEntryPoint.kt | 2 +- .../impl/root/RoomDirectoryEvents.kt | 4 ++ .../impl/root/RoomDirectoryNode.kt | 6 +-- .../impl/root/RoomDirectoryPresenter.kt | 22 ++++++++++ .../impl/root/RoomDirectoryState.kt | 3 ++ .../impl/root/RoomDirectoryStateProvider.kt | 6 +-- .../impl/root/RoomDirectoryView.kt | 18 ++++++-- .../libraries/matrix/api/MatrixClient.kt | 1 + .../libraries/matrix/impl/RustMatrixClient.kt | 44 +++++++++++++++---- 10 files changed, 89 insertions(+), 19 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 82a645b636..a9dd485285 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -389,7 +389,7 @@ class LoggedInFlowNode @AssistedInject constructor( NavTarget.RoomDirectorySearch -> { roomDirectoryEntryPoint.nodeBuilder(this, buildContext) .callback(object : RoomDirectoryEntryPoint.Callback { - override fun onJoinRoom(roomId: RoomId) { + override fun onOpenRoom(roomId: RoomId) { coroutineScope.launch { attachRoom(roomId) } } }) diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt index a665f9f23c..8fbf342697 100644 --- a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt @@ -32,7 +32,7 @@ interface RoomDirectoryEntryPoint : FeatureEntryPoint { } interface Callback : Plugin { - fun onJoinRoom(roomId: RoomId) + fun onOpenRoom(roomId: RoomId) } } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt index f105a19c53..37e0ffb3c6 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt @@ -16,7 +16,11 @@ package io.element.android.features.roomdirectory.impl.root +import io.element.android.libraries.matrix.api.core.RoomId + sealed interface RoomDirectoryEvents { + data class JoinRoom(val roomId: RoomId) : RoomDirectoryEvents data class Search(val query: String) : RoomDirectoryEvents data object LoadMore : RoomDirectoryEvents + data object JoinRoomDismissError : RoomDirectoryEvents } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt index 5c2cbf654c..c99d30640a 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt @@ -36,9 +36,9 @@ class RoomDirectoryNode @AssistedInject constructor( private val presenter: RoomDirectoryPresenter, ) : Node(buildContext, plugins = plugins) { - private fun onJoinRoom(roomId: RoomId) { + private fun onRoomJoined(roomId: RoomId) { plugins().forEach { - it.onJoinRoom(roomId) + it.onOpenRoom(roomId) } } @@ -47,7 +47,7 @@ class RoomDirectoryNode @AssistedInject constructor( val state = presenter.present() RoomDirectoryView( state = state, - onJoinRoom = ::onJoinRoom, + onRoomJoined = ::onRoomJoined, onBackPressed = ::navigateUp, modifier = modifier ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt index bfd88f4e28..9e91193114 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt @@ -18,6 +18,7 @@ package io.element.android.features.roomdirectory.impl.root import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -27,13 +28,17 @@ 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.libraries.architecture.AsyncAction 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.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.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -41,6 +46,7 @@ import javax.inject.Inject class RoomDirectoryPresenter @Inject constructor( private val dispatchers: CoroutineDispatchers, + private val matrixClient: MatrixClient, roomDirectoryService: RoomDirectoryService, ) : Presenter { @@ -56,6 +62,9 @@ class RoomDirectoryPresenter @Inject constructor( val hasMoreToLoad by produceState(initialValue = true, allRooms) { value = roomDirectoryList.hasMoreToLoad() } + val joinRoomAction: MutableState> = remember { + mutableStateOf(AsyncAction.Uninitialized) + } val coroutineScope = rememberCoroutineScope() LaunchedEffect(searchQuery) { roomDirectoryList.filter(searchQuery, 20) @@ -70,6 +79,12 @@ class RoomDirectoryPresenter @Inject constructor( is RoomDirectoryEvents.Search -> { 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, roomDescriptions = allRooms, displayLoadMoreIndicator = hasMoreToLoad, + joinRoomAction = joinRoomAction.value, eventSink = ::handleEvents ) } + private fun CoroutineScope.joinRoom(state: MutableState>, roomId: RoomId) = launch { + state.runUpdatingState { + matrixClient.joinRoom(roomId) + } + } + @Composable private fun RoomDirectoryList.collectItemsAsState() = remember { items.map { list -> diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt index 1afd53f146..937f78a69b 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt @@ -17,12 +17,15 @@ package io.element.android.features.roomdirectory.impl.root 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 data class RoomDirectoryState( val query: String, val roomDescriptions: ImmutableList, val displayLoadMoreIndicator: Boolean, + val joinRoomAction: AsyncAction, val eventSink: (RoomDirectoryEvents) -> Unit ) { val displayEmptyState = roomDescriptions.isEmpty() && !displayLoadMoreIndicator diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt index 30acb14dba..8261959016 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt @@ -18,9 +18,9 @@ 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.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.avatar.AvatarData 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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -63,13 +63,13 @@ open class RoomDirectorySearchStateProvider : PreviewParameterProvider = persistentListOf(), - searchResults: SearchBarResultState> = SearchBarResultState.Initial(), + joinRoomAction: AsyncAction = AsyncAction.Uninitialized, ) = RoomDirectoryState( query = query, roomDescriptions = roomDescriptions, displayLoadMoreIndicator = displayLoadMoreIndicator, + joinRoomAction = joinRoomAction, eventSink = {}, ) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index fecebf572f..bfd25d7f3d 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -47,6 +47,7 @@ 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.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview @@ -66,10 +67,15 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun RoomDirectoryView( state: RoomDirectoryState, + onRoomJoined: (RoomId) -> Unit, onBackPressed: () -> Unit, - onJoinRoom: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { + + fun joinRoom(roomId: RoomId) { + state.eventSink(RoomDirectoryEvents.JoinRoom(roomId)) + } + Scaffold( modifier = modifier, topBar = { @@ -78,13 +84,19 @@ fun RoomDirectoryView( content = { padding -> RoomDirectoryContent( state = state, - onResultClicked = onJoinRoom, + onResultClicked = ::joinRoom, modifier = Modifier .padding(padding) .consumeWindowInsets(padding) ) } ) + AsyncActionView( + async = state.joinRoomAction, + onSuccess = onRoomJoined, + onErrorDismiss = { + state.eventSink(RoomDirectoryEvents.JoinRoomDismissError) + }) } @OptIn(ExperimentalMaterial3Api::class) @@ -295,7 +307,7 @@ private fun RoomDirectoryRoomRow( fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectoryState) = ElementPreview { RoomDirectoryView( state = state, - onJoinRoom = {}, + onRoomJoined = {}, onBackPressed = {}, ) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 43ca8f6687..e6c536ee72 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -59,6 +59,7 @@ interface MatrixClient : Closeable { suspend fun setDisplayName(displayName: String): Result suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result suspend fun removeAvatar(): Result + suspend fun joinRoom(roomId: RoomId): Result fun syncService(): SyncService fun sessionVerificationService(): SessionVerificationService fun pushersService(): PushersService diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 280b56724b..277288205f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -73,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 @@ -103,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 @@ -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? { val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value) val fullRoom = cachedRoomListItem?.fullRoomWithTimeline(filter = eventFilters) @@ -369,14 +388,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 } @@ -425,6 +441,18 @@ class RustMatrixClient( runCatching { client.removeAvatar() } } + override suspend fun joinRoom(roomId: RoomId): Result = 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 From 153e88dade103a99be2faa044e89e31559928158 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 26 Mar 2024 15:18:16 +0100 Subject: [PATCH 10/21] RoomList Search : do not persist isActive when leaving composition --- .../roomlist/impl/search/RoomListSearchPresenter.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt index 78bcda07f1..d751651ef4 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt @@ -21,7 +21,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Presenter import kotlinx.collections.immutable.persistentListOf @@ -32,10 +32,11 @@ class RoomListSearchPresenter @Inject constructor( ) : Presenter { @Composable override fun present(): RoomListSearchState { - var isSearchActive by rememberSaveable { + // Do not use rememberSaveable so that search is not active when the user navigates back to the screen + var isSearchActive by remember { mutableStateOf(false) } - var searchQuery by rememberSaveable { + var searchQuery by remember { mutableStateOf("") } From b90081800133b8789cb671393a5cb6d8690ffd97 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 27 Mar 2024 12:51:36 +0100 Subject: [PATCH 11/21] RoomDirectory : continue improving interactions --- features/roomdirectory/api/build.gradle.kts | 1 + .../roomdirectory/api/RoomDescription.kt | 28 +++++++ .../impl/root/RoomDirectoryPresenter.kt | 56 ++++++++------ .../impl/root/RoomDirectoryState.kt | 4 +- .../impl/root/RoomDirectoryStateProvider.kt | 8 +- .../impl/root/RoomDirectoryView.kt | 8 +- ...scriptionUiModel.kt => RoomDescription.kt} | 20 ++--- .../impl/root/model/RoomDirectoryListState.kt | 34 +++++++++ .../api/roomdirectory/RoomDirectoryList.kt | 8 +- .../api/roomdirectory/RoomDirectoryService.kt | 4 +- .../libraries/matrix/impl/RustMatrixClient.kt | 4 +- .../roomdirectory/RustRoomDirectoryList.kt | 73 +++++++++++-------- .../roomdirectory/RustRoomDirectoryService.kt | 6 +- .../roomdirectory/FakeRoomDirectoryService.kt | 3 +- 14 files changed, 171 insertions(+), 86 deletions(-) create mode 100644 features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt rename features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/{RoomDescriptionUiModel.kt => RoomDescription.kt} (71%) create mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt diff --git a/features/roomdirectory/api/build.gradle.kts b/features/roomdirectory/api/build.gradle.kts index c55c647e24..04a813bd0f 100644 --- a/features/roomdirectory/api/build.gradle.kts +++ b/features/roomdirectory/api/build.gradle.kts @@ -25,4 +25,5 @@ android { dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.designsystem) } diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt new file mode 100644 index 0000000000..5b945b2a7d --- /dev/null +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt @@ -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, +) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt index 9e91193114..2247a82b55 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt @@ -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 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 class RoomDirectoryPresenter @Inject constructor( private val dispatchers: CoroutineDispatchers, private val matrixClient: MatrixClient, - roomDirectoryService: RoomDirectoryService, + private val roomDirectoryService: RoomDirectoryService, ) : Presenter { - private val roomDirectoryList = roomDirectoryService.createRoomDirectoryList() - @Composable override fun present(): RoomDirectoryState { - + var loadingMore by remember { + mutableStateOf(false) + } var searchQuery by rememberSaveable { - mutableStateOf("") + mutableStateOf(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> = 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( } 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( } @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) } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt index 937f78a69b..526139338d 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt @@ -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, + val roomDescriptions: ImmutableList, val displayLoadMoreIndicator: Boolean, val joinRoomAction: AsyncAction, val eventSink: (RoomDirectoryEvents) -> Unit diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt index 8261959016..ff12bc7ee6 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt @@ -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 = persistentListOf(), + roomDescriptions: ImmutableList = persistentListOf(), joinRoomAction: AsyncAction = AsyncAction.Uninitialized, ) = RoomDirectoryState( query = query, diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index bfd25d7f3d..6b5d94eb63 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/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 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( @Composable private fun RoomDirectoryRoomList( - roomDescriptions: ImmutableList, + roomDescriptions: ImmutableList, displayLoadMoreIndicator: Boolean, displayEmptyState: Boolean, onResultClicked: (RoomId) -> Unit, @@ -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( @Composable private fun RoomDirectoryRoomRow( - roomDescription: RoomDescriptionUiModel, + roomDescription: RoomDescription, onClick: (RoomId) -> Unit, modifier: Modifier = Modifier, ) { diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescriptionUiModel.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt similarity index 71% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescriptionUiModel.kt rename to features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt index c7424eebf0..405d4241b2 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescriptionUiModel.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt @@ -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, ) } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt new file mode 100644 index 0000000000..d85295ca7e --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt @@ -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, +) { + + companion object { + val Default = RoomDirectoryListState( + hasMoreToLoad = true, + items = persistentListOf() + ) + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt index f232983620..2311c5afee 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt @@ -21,6 +21,10 @@ import kotlinx.coroutines.flow.Flow interface RoomDirectoryList { suspend fun filter(filter: String?, batchSize: Int): Result suspend fun loadMore(): Result - suspend fun hasMoreToLoad(): Boolean - val items: Flow> + val state: Flow + + data class State( + val hasMoreToLoad: Boolean, + val items: List, + ) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt index fe9beb27d6..26df48be71 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt @@ -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 } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 277288205f..fffdfa8c7c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -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( private val roomDirectoryService = RustRoomDirectoryService( client = client, - sessionCoroutineScope = sessionCoroutineScope, sessionDispatcher = sessionDispatcher, ) @@ -441,7 +441,7 @@ class RustMatrixClient( runCatching { client.removeAvatar() } } - override suspend fun joinRoom(roomId: RoomId): Result = withContext(sessionDispatcher) { + override suspend fun joinRoom(roomId: RoomId): Result = withContext(sessionDispatcher) { runCatching { client.joinRoomById(roomId.value).destroy() try { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt index 7b7296db8e..f2e26f51a7 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt +++ b/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 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>(replay = 1) - private val processor = RoomDirectorySearchProcessor(_items, sessionDispatcher, RoomDescriptionMapper()) + private val hasMoreToLoad = MutableStateFlow(true) + private val items = MutableSharedFlow>(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 { - 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 { - return try { + return execute { inner.nextPage() + } + } + + private suspend fun execute(action: suspend () -> Unit): Result { + 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> = _items.debounce(200.milliseconds) + override val state: Flow = + combine(hasMoreToLoad, items) { hasMoreToLoad, items -> + RoomDirectoryList.State( + hasMoreToLoad = hasMoreToLoad, + items = items + ) + } + .flowOn(coroutineContext) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt index 7022b2e21b..7627f90261 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt +++ b/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 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) } - } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt index 9281a41bc8..25f714cfde 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt +++ b/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 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") } } From d632e216b7690037c81dac06462e1d0b225249c6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 27 Mar 2024 12:57:50 +0100 Subject: [PATCH 12/21] RoomDirectory : branch feature flag --- .../impl/search/RoomListSearchPresenter.kt | 5 +++++ .../roomlist/impl/search/RoomListSearchState.kt | 5 ++++- .../impl/search/RoomListSearchStateProvider.kt | 3 +++ .../roomlist/impl/search/RoomListSearchView.kt | 16 +++++++--------- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt index d751651ef4..4ae7f091fe 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt @@ -24,11 +24,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import kotlinx.collections.immutable.persistentListOf import javax.inject.Inject class RoomListSearchPresenter @Inject constructor( private val dataSource: RoomListSearchDataSource, + private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable override fun present(): RoomListSearchState { @@ -63,12 +66,14 @@ class RoomListSearchPresenter @Inject constructor( } } + val isRoomDirectorySearchEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch).collectAsState(initial = false) val searchResults by dataSource.roomSummaries.collectAsState(initial = persistentListOf()) return RoomListSearchState( isSearchActive = isSearchActive, query = searchQuery, results = searchResults, + isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, eventSink = ::handleEvents ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt index c4b24dc798..92e70ad039 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt @@ -23,5 +23,8 @@ data class RoomListSearchState( val isSearchActive: Boolean, val query: String, val results: ImmutableList, + val isRoomDirectorySearchEnabled: Boolean, val eventSink: (RoomListSearchEvents) -> Unit -) +) { + val displayRoomDirectorySearch = query.isEmpty() && isRoomDirectorySearchEnabled +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt index ae722a4b04..c4dcbab1ec 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt @@ -26,6 +26,7 @@ class RoomListSearchStateProvider : PreviewParameterProvider get() = sequenceOf( aRoomListSearchState(), + aRoomListSearchState(isRoomDirectorySearchEnabled = true), aRoomListSearchState( isSearchActive = true, query = "Test", @@ -38,10 +39,12 @@ fun aRoomListSearchState( isSearchActive: Boolean = false, query: String = "", results: ImmutableList = persistentListOf(), + isRoomDirectorySearchEnabled: Boolean = false, eventSink: (RoomListSearchEvents) -> Unit = { }, ) = RoomListSearchState( isSearchActive = isSearchActive, query = query, results = results, + isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, eventSink = eventSink, ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt index 9af30fbdcd..53528e0558 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt @@ -26,8 +26,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.QrCode import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextFieldDefaults @@ -133,8 +131,8 @@ private fun RoomListSearchContent( val focusRequester = FocusRequester() TextField( modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), + .fillMaxWidth() + .focusRequester(focusRequester), value = filter, singleLine = true, onValueChange = { state.eventSink(RoomListSearchEvents.QueryChanged(it)) }, @@ -173,14 +171,14 @@ private fun RoomListSearchContent( ) { padding -> Column( modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) { - if(state.query.isEmpty()){ + if (state.displayRoomDirectorySearch) { RoomDirectorySearchButton( modifier = Modifier - .fillMaxWidth() - .padding(vertical = 24.dp, horizontal = 16.dp), + .fillMaxWidth() + .padding(vertical = 24.dp, horizontal = 16.dp), onClick = onRoomDirectorySearchClicked ) } From 3f1f76474570090575f3914a78ec6dc741411900 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 28 Mar 2024 17:03:34 +0100 Subject: [PATCH 13/21] Room directory : add tests and cleanup --- .idea/kotlinc.xml | 2 +- .../api/RoomDirectoryEntryPoint.kt | 2 - features/roomdirectory/impl/build.gradle.kts | 9 + .../impl/DefaultRoomDirectoryEntryPoint.kt | 2 - .../impl/root/RoomDirectoryNode.kt | 1 - .../impl/root/RoomDirectoryPresenter.kt | 11 +- .../impl/root/RoomDirectoryStateProvider.kt | 63 +++--- .../impl/root/RoomDirectoryView.kt | 48 ++--- .../roomdirectory/impl/root/di/JoinRoom.kt | 32 +++ .../impl/root/model/RoomDirectoryListState.kt | 1 - .../roomdirectory/impl/root/FakeJoinRoom.kt | 26 +++ .../impl/root/RoomDirectoryPresenterTest.kt | 182 ++++++++++++++++++ .../impl/root/RoomDirectoryViewTest.kt | 112 +++++++++++ .../roomlist/impl/RoomListViewTest.kt | 2 + .../search/RoomListSearchPresenterTests.kt | 22 ++- .../libraries/matrix/impl/RustMatrixClient.kt | 3 +- .../roomdirectory/RoomDescriptionMapper.kt | 1 - .../RoomDirectorySearchProcessor.kt | 1 - .../roomdirectory/RustRoomDirectoryList.kt | 1 - .../roomdirectory/RustRoomDirectoryService.kt | 1 - .../libraries/matrix/test/FakeMatrixClient.kt | 5 + .../roomdirectory/FakeRoomDirectoryList.kt | 31 +++ .../roomdirectory/FakeRoomDirectoryService.kt | 8 +- .../roomdirectory/RoomDescriptionFixture.kt | 41 ++++ .../android/libraries/testtags/TestTags.kt | 5 + .../android/samples/minimal/RoomListScreen.kt | 4 +- .../android/tests/testutils/PresenterTest.kt | 34 ++++ 27 files changed, 573 insertions(+), 77 deletions(-) create mode 100644 features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/di/JoinRoom.kt create mode 100644 features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/FakeJoinRoom.kt create mode 100644 features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt create mode 100644 features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryList.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt create mode 100644 tests/testutils/src/main/kotlin/io/element/android/tests/testutils/PresenterTest.kt diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 8d81632f83..fe63bb677d 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt index 8fbf342697..5a693a4a83 100644 --- a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt @@ -23,7 +23,6 @@ import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.matrix.api.core.RoomId interface RoomDirectoryEntryPoint : FeatureEntryPoint { - fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder interface NodeBuilder { @@ -35,4 +34,3 @@ interface RoomDirectoryEntryPoint : FeatureEntryPoint { fun onOpenRoom(roomId: RoomId) } } - diff --git a/features/roomdirectory/impl/build.gradle.kts b/features/roomdirectory/impl/build.gradle.kts index 9d56802700..d26a75ca8c 100644 --- a/features/roomdirectory/impl/build.gradle.kts +++ b/features/roomdirectory/impl/build.gradle.kts @@ -25,6 +25,11 @@ plugins { android { namespace = "io.element.android.features.roomdirectory.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } anvil { @@ -41,13 +46,17 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.testtags) testImplementation(libs.test.junit) + testImplementation(libs.androidx.compose.ui.test.junit) + testImplementation(libs.test.robolectric) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt index 11b5b1b27d..c15a748a9e 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt @@ -28,12 +28,10 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultRoomDirectoryEntryPoint @Inject constructor() : RoomDirectoryEntryPoint { - override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDirectoryEntryPoint.NodeBuilder { val plugins = ArrayList() return object : RoomDirectoryEntryPoint.NodeBuilder { - override fun callback(callback: RoomDirectoryEntryPoint.Callback): RoomDirectoryEntryPoint.NodeBuilder { plugins += callback return this diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt index c99d30640a..dc3581589e 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt @@ -35,7 +35,6 @@ class RoomDirectoryNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: RoomDirectoryPresenter, ) : Node(buildContext, plugins = plugins) { - private fun onRoomJoined(roomId: RoomId) { plugins().forEach { it.onOpenRoom(roomId) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt index 2247a82b55..95cfa8166b 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt @@ -26,13 +26,13 @@ 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.di.JoinRoom 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 import io.element.android.libraries.core.coroutine.CoroutineDispatchers -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 @@ -46,10 +46,9 @@ import javax.inject.Inject class RoomDirectoryPresenter @Inject constructor( private val dispatchers: CoroutineDispatchers, - private val matrixClient: MatrixClient, + private val joinRoom: JoinRoom, private val roomDirectoryService: RoomDirectoryService, ) : Presenter { - @Composable override fun present(): RoomDirectoryState { var loadingMore by remember { @@ -68,9 +67,9 @@ class RoomDirectoryPresenter @Inject constructor( } LaunchedEffect(searchQuery) { if (searchQuery == null) return@LaunchedEffect - //debounce search query + // debounce search query delay(300) - //cancel load more right away + // cancel load more right away loadingMore = false roomDirectoryList.filter(searchQuery, 20) } @@ -108,7 +107,7 @@ class RoomDirectoryPresenter @Inject constructor( private fun CoroutineScope.joinRoom(state: MutableState>, roomId: RoomId) = launch { state.runUpdatingState { - matrixClient.joinRoom(roomId) + joinRoom(roomId) } } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt index ff12bc7ee6..3fa7877b6f 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt @@ -25,39 +25,14 @@ import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -open class RoomDirectorySearchStateProvider : PreviewParameterProvider { +open class RoomDirectoryStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomDirectoryState(), aRoomDirectoryState( query = "Element", - roomDescriptions = persistentListOf( - RoomDescription( - roomId = RoomId("@exa:matrix.org"), - name = "Element X Android", - description = "Element X is a secure, private and decentralized messenger.", - avatarData = AvatarData( - id = "@exa:matrix.org", - name = "Element X Android", - url = null, - size = AvatarSize.RoomDirectoryItem - ), - canBeJoined = true, - ), - RoomDescription( - roomId = RoomId("@exi:matrix.org"), - name = "Element X iOS", - description = "Element X is a secure, private and decentralized messenger.", - avatarData = AvatarData( - id = "@exi:matrix.org", - name = "Element X iOS", - url = null, - size = AvatarSize.RoomDirectoryItem - ), - canBeJoined = false, - ) - ) - ), + roomDescriptions = aRoomDescriptionList(), + ) ) } @@ -66,10 +41,40 @@ fun aRoomDirectoryState( displayLoadMoreIndicator: Boolean = false, roomDescriptions: ImmutableList = persistentListOf(), joinRoomAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (RoomDirectoryEvents) -> Unit = {}, ) = RoomDirectoryState( query = query, roomDescriptions = roomDescriptions, displayLoadMoreIndicator = displayLoadMoreIndicator, joinRoomAction = joinRoomAction, - eventSink = {}, + eventSink = eventSink, ) + +fun aRoomDescriptionList(): ImmutableList { + return persistentListOf( + RoomDescription( + roomId = RoomId("!exa:matrix.org"), + name = "Element X Android", + description = "Element X is a secure, private and decentralized messenger.", + avatarData = AvatarData( + id = "!exa:matrix.org", + name = "Element X Android", + url = null, + size = AvatarSize.RoomDirectoryItem + ), + canBeJoined = true, + ), + RoomDescription( + roomId = RoomId("!exi:matrix.org"), + name = "Element X iOS", + description = "Element X is a secure, private and decentralized messenger.", + avatarData = AvatarData( + id = "!exi:matrix.org", + name = "Element X iOS", + url = null, + size = AvatarSize.RoomDirectoryItem + ), + canBeJoined = false, + ) + ) +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index 6b5d94eb63..a40855fcc3 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter @@ -61,6 +62,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList @@ -71,7 +73,6 @@ fun RoomDirectoryView( onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { - fun joinRoom(roomId: RoomId) { state.eventSink(RoomDirectoryEvents.JoinRoom(roomId)) } @@ -86,8 +87,8 @@ fun RoomDirectoryView( state = state, onResultClicked = ::joinRoom, modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) } ) @@ -96,7 +97,8 @@ fun RoomDirectoryView( onSuccess = onRoomJoined, onErrorDismiss = { state.eventSink(RoomDirectoryEvents.JoinRoomDismissError) - }) + } + ) } @OptIn(ExperimentalMaterial3Api::class) @@ -171,7 +173,7 @@ private fun RoomDirectoryRoomList( if (displayLoadMoreIndicator) { item { LoadMoreIndicator(modifier = Modifier.fillMaxWidth()) - LaunchedEffect(Unit) { + LaunchedEffect(onReachedLoadMore) { onReachedLoadMore() } } @@ -182,10 +184,10 @@ private fun RoomDirectoryRoomList( @Composable private fun LoadMoreIndicator(modifier: Modifier = Modifier) { Box( - modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(24.dp), + modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(24.dp), contentAlignment = Alignment.Center, ) { CircularProgressIndicator( @@ -213,7 +215,7 @@ private fun SearchTextField( ) { val focusManager = LocalFocusManager.current TextField( - modifier = modifier, + modifier = modifier.testTag(TestTags.searchTextField.value), textStyle = ElementTheme.typography.fontBodyLgRegular, singleLine = true, value = query, @@ -255,14 +257,14 @@ private fun RoomDirectoryRoomRow( ) { Row( modifier = modifier - .fillMaxWidth() - .clickable { onClick(roomDescription.roomId) } - .padding( - top = 12.dp, - bottom = 12.dp, - start = 16.dp, - ) - .height(IntrinsicSize.Min), + .fillMaxWidth() + .clickable { onClick(roomDescription.roomId) } + .padding( + top = 12.dp, + bottom = 12.dp, + start = 16.dp, + ) + .height(IntrinsicSize.Min), ) { Avatar( avatarData = roomDescription.avatarData, @@ -270,8 +272,8 @@ private fun RoomDirectoryRoomRow( ) Column( modifier = Modifier - .weight(1f) - .padding(start = 16.dp) + .weight(1f) + .padding(start = 16.dp) ) { Text( text = roomDescription.name, @@ -293,8 +295,8 @@ private fun RoomDirectoryRoomRow( text = stringResource(id = CommonStrings.action_join), color = ElementTheme.colors.textSuccessPrimary, modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 4.dp, end = 12.dp) + .align(Alignment.CenterVertically) + .padding(start = 4.dp, end = 12.dp) ) } else { Spacer(modifier = Modifier.width(24.dp)) @@ -304,7 +306,7 @@ private fun RoomDirectoryRoomRow( @PreviewsDayNight @Composable -fun RoomDirectorySearchViewLightPreview(@PreviewParameter(RoomDirectorySearchStateProvider::class) state: RoomDirectoryState) = ElementPreview { +internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview { RoomDirectoryView( state = state, onRoomJoined = {}, diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/di/JoinRoom.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/di/JoinRoom.kt new file mode 100644 index 0000000000..983d2a1dd2 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/di/JoinRoom.kt @@ -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.features.roomdirectory.impl.root.di + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import javax.inject.Inject + +interface JoinRoom { + suspend operator fun invoke(roomId: RoomId): Result +} + +@ContributesBinding(SessionScope::class) +class DefaultJoinRoom @Inject constructor(private val client: MatrixClient) : JoinRoom { + override suspend fun invoke(roomId: RoomId) = client.joinRoom(roomId) +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt index d85295ca7e..60f344f67b 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt @@ -24,7 +24,6 @@ internal data class RoomDirectoryListState( val hasMoreToLoad: Boolean, val items: ImmutableList, ) { - companion object { val Default = RoomDirectoryListState( hasMoreToLoad = true, diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/FakeJoinRoom.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/FakeJoinRoom.kt new file mode 100644 index 0000000000..3f4d17aefd --- /dev/null +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/FakeJoinRoom.kt @@ -0,0 +1,26 @@ +/* + * 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 + +import io.element.android.features.roomdirectory.impl.root.di.JoinRoom +import io.element.android.libraries.matrix.api.core.RoomId + +class FakeJoinRoom( + var lambda: (RoomId) -> Result = { Result.success(it) } +) : JoinRoom { + override suspend fun invoke(roomId: RoomId) = lambda(roomId) +} diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt new file mode 100644 index 0000000000..eefafc86e1 --- /dev/null +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt @@ -0,0 +1,182 @@ +/* + * 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 + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomdirectory.impl.root.di.JoinRoom +import io.element.android.libraries.architecture.AsyncAction +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 io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryList +import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryService +import io.element.android.libraries.matrix.test.roomdirectory.aRoomDescription +import io.element.android.tests.testutils.lambda.any +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import io.element.android.tests.testutils.test +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) class RoomDirectoryPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createRoomDirectoryPresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.query).isEmpty() + assertThat(initialState.displayEmptyState).isFalse() + assertThat(initialState.joinRoomAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(initialState.roomDescriptions).isEmpty() + assertThat(initialState.displayLoadMoreIndicator).isTrue() + } + } + + @Test + fun `present - room directory list emits empty state`() = runTest { + val directoryListStateFlow = MutableSharedFlow(replay = 1) + val roomDirectoryList = FakeRoomDirectoryList(directoryListStateFlow) + val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } + val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) + presenter.test { + skipItems(1) + directoryListStateFlow.emit( + RoomDirectoryList.State(false, emptyList()) + ) + awaitItem().also { state -> + assertThat(state.displayEmptyState).isTrue() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - room directory list emits non-empty state`() = runTest { + val directoryListStateFlow = MutableSharedFlow(replay = 1) + val roomDirectoryList = FakeRoomDirectoryList(directoryListStateFlow) + val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } + val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) + presenter.test { + skipItems(1) + directoryListStateFlow.emit( + RoomDirectoryList.State( + hasMoreToLoad = true, + items = listOf(aRoomDescription()) + ) + ) + awaitItem().also { state -> + assertThat(state.displayEmptyState).isFalse() + assertThat(state.roomDescriptions).hasSize(1) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - emit search event`() = runTest { + val filterLambda = lambdaRecorder { _: String?, _: Int -> + Result.success(Unit) + } + val roomDirectoryList = FakeRoomDirectoryList(filterLambda = filterLambda) + val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } + val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) + presenter.test { + awaitItem().also { state -> + state.eventSink(RoomDirectoryEvents.Search("test")) + } + awaitItem().also { state -> + assertThat(state.query).isEqualTo("test") + } + advanceUntilIdle() + cancelAndIgnoreRemainingEvents() + } + assert(filterLambda) + .isCalledOnce() + .with(value("test"), any()) + } + + @Test + fun `present - emit load more event`() = runTest { + val loadMoreLambda = lambdaRecorder { -> + Result.success(Unit) + } + val roomDirectoryList = FakeRoomDirectoryList(loadMoreLambda = loadMoreLambda) + val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } + val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) + presenter.test { + awaitItem().also { state -> + state.eventSink(RoomDirectoryEvents.LoadMore) + } + advanceUntilIdle() + cancelAndIgnoreRemainingEvents() + } + assert(loadMoreLambda) + .isCalledOnce() + .withNoParameter() + } + + @Test + fun `present - emit join room event`() = runTest { + val joinRoomSuccess = lambdaRecorder { roomId: RoomId -> + Result.success(roomId) + } + val joinRoomFailure = lambdaRecorder { roomId: RoomId -> + Result.failure(RuntimeException("Failed to join room $roomId")) + } + val fakeJoinRoom = FakeJoinRoom(joinRoomSuccess) + val presenter = createRoomDirectoryPresenter(joinRoom = fakeJoinRoom) + presenter.test { + awaitItem().also { state -> + state.eventSink(RoomDirectoryEvents.JoinRoom(A_ROOM_ID)) + } + awaitItem().also { state -> + assertThat(state.joinRoomAction).isEqualTo(AsyncAction.Success(A_ROOM_ID)) + fakeJoinRoom.lambda = joinRoomFailure + state.eventSink(RoomDirectoryEvents.JoinRoom(A_ROOM_ID)) + } + awaitItem().also { state -> + assertThat(state.joinRoomAction).isInstanceOf(AsyncAction.Failure::class.java) + } + } + assert(joinRoomSuccess) + .isCalledOnce() + .with(value(A_ROOM_ID)) + assert(joinRoomFailure) + .isCalledOnce() + .with(value(A_ROOM_ID)) + } + + private fun TestScope.createRoomDirectoryPresenter( + roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService( + createRoomDirectoryListFactory = { FakeRoomDirectoryList() } + ), + joinRoom: JoinRoom = FakeJoinRoom { Result.success(it) }, + ): RoomDirectoryPresenter { + return RoomDirectoryPresenter( + dispatchers = testCoroutineDispatchers(), + joinRoom = joinRoom, + roomDirectoryService = roomDirectoryService, + ) + } +} diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt new file mode 100644 index 0000000000..bcac35fc3a --- /dev/null +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt @@ -0,0 +1,112 @@ +/* + * 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 + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.testtags.TestTags +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RoomDirectoryViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `typing text in search field emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomDirectoryView( + state = aRoomDirectoryState( + eventSink = eventsRecorder, + ) + ) + rule.onNodeWithTag(TestTags.searchTextField.value).performTextInput( + text = "Test" + ) + eventsRecorder.assertSingle(RoomDirectoryEvents.Search("Test")) + } + + @Test + fun `clicking on room item emits the expected Event`() { + val eventsRecorder = EventsRecorder() + val state = aRoomDirectoryState( + roomDescriptions = aRoomDescriptionList(), + eventSink = eventsRecorder, + ) + rule.setRoomDirectoryView(state = state) + val clickedRoom = state.roomDescriptions.first() + rule.onNodeWithText(clickedRoom.name).performClick() + eventsRecorder.assertSingle(RoomDirectoryEvents.JoinRoom(clickedRoom.roomId)) + } + + @Test + fun `composing load more indicator emits expected Event`() { + val eventsRecorder = EventsRecorder() + val state = aRoomDirectoryState( + displayLoadMoreIndicator = true, + eventSink = eventsRecorder, + ) + rule.setRoomDirectoryView(state = state) + eventsRecorder.assertSingle(RoomDirectoryEvents.LoadMore) + } + + @Test + fun `when joining room with success then onRoomJoined lambda is called once`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val roomDescriptions = aRoomDescriptionList() + val joinedRoomId = roomDescriptions.first().roomId + val state = aRoomDirectoryState( + joinRoomAction = AsyncAction.Success(joinedRoomId), + roomDescriptions = roomDescriptions, + eventSink = eventsRecorder, + ) + ensureCalledOnceWithParam(joinedRoomId) { callback -> + rule.setRoomDirectoryView( + state = state, + onRoomJoined = callback, + ) + } + } +} + +private fun AndroidComposeTestRule.setRoomDirectoryView( + state: RoomDirectoryState, + onBackPressed: () -> Unit = EnsureNeverCalled(), + onRoomJoined: (RoomId) -> Unit = EnsureNeverCalledWithParam(), +) { + setContent { + RoomDirectoryView( + state = state, + onRoomJoined = onRoomJoined, + onBackPressed = onBackPressed, + ) + } +} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt index f545b27860..c860b6fc42 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt @@ -191,6 +191,7 @@ private fun AndroidComposeTestRule.setRoomL onInvitesClicked: () -> Unit = EnsureNeverCalled(), onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(), + onRoomDirectorySearchClicked: () -> Unit = EnsureNeverCalled(), ) { setContent { RoomListView( @@ -203,6 +204,7 @@ private fun AndroidComposeTestRule.setRoomL onInvitesClicked = onInvitesClicked, onRoomSettingsClicked = onRoomSettingsClicked, onMenuActionClicked = onMenuActionClicked, + onRoomDirectorySearchClicked = onRoomDirectorySearchClicked, ) } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt index d3fc434f25..b3463c549f 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt @@ -23,6 +23,9 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomSummary @@ -128,10 +131,26 @@ class RoomListSearchPresenterTests { } } } + + @Test + fun `present - room directory search`() = runTest { + val featureFlagService = FakeFeatureFlagService() + featureFlagService.setFeatureEnabled(FeatureFlags.RoomDirectorySearch, true) + val presenter = createRoomListSearchPresenter(featureFlagService = featureFlagService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + awaitItem().let { state -> + assertThat(state.isRoomDirectorySearchEnabled).isTrue() + } + } + } } fun TestScope.createRoomListSearchPresenter( roomListService: RoomListService = FakeRoomListService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), ): RoomListSearchPresenter { return RoomListSearchPresenter( dataSource = RoomListSearchDataSource( @@ -141,6 +160,7 @@ fun TestScope.createRoomListSearchPresenter( roomLastMessageFormatter = FakeRoomLastMessageFormatter(), ), coroutineDispatchers = testCoroutineDispatchers(), - ) + ), + featureFlagService = featureFlagService, ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index fffdfa8c7c..481c094fd4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -76,7 +76,6 @@ 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 @@ -441,7 +440,7 @@ class RustMatrixClient( runCatching { client.removeAvatar() } } - override suspend fun joinRoom(roomId: RoomId): Result = withContext(sessionDispatcher) { + override suspend fun joinRoom(roomId: RoomId): Result = withContext(sessionDispatcher) { runCatching { client.joinRoomById(roomId.value).destroy() try { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt index 52b8e1ba98..91b84e48c0 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt @@ -22,7 +22,6 @@ 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), diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt index f060635cdf..aff631d6b4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt @@ -30,7 +30,6 @@ class RoomDirectorySearchProcessor( private val coroutineContext: CoroutineContext, private val roomDescriptionMapper: RoomDescriptionMapper, ) { - private val mutex = Mutex() suspend fun postUpdates(updates: List) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt index f2e26f51a7..9b444d5ce5 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt @@ -35,7 +35,6 @@ class RustRoomDirectoryList( coroutineScope: CoroutineScope, private val coroutineContext: CoroutineContext, ) : RoomDirectoryList { - private val hasMoreToLoad = MutableStateFlow(true) private val items = MutableSharedFlow>(replay = 1) private val processor = RoomDirectorySearchProcessor(items, coroutineContext, RoomDescriptionMapper()) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt index 7627f90261..2939001b21 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt @@ -26,7 +26,6 @@ class RustRoomDirectoryService( private val client: Client, private val sessionDispatcher: CoroutineDispatcher, ) : RoomDirectoryService { - override fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList { return RustRoomDirectoryList(client.roomDirectorySearch(), scope, sessionDispatcher) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 1ac68a5fa5..247bc4bbe9 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -94,6 +94,9 @@ class FakeMatrixClient( private var setDisplayNameResult: Result = Result.success(Unit) private var uploadAvatarResult: Result = Result.success(Unit) private var removeAvatarResult: Result = Result.success(Unit) + var joinRoomLambda: suspend (RoomId) -> Result = { + Result.success(it) + } override suspend fun getRoom(roomId: RoomId): MatrixRoom? { return getRoomResults[roomId] @@ -181,6 +184,8 @@ class FakeMatrixClient( return removeAvatarResult } + override suspend fun joinRoom(roomId: RoomId): Result = joinRoomLambda(roomId) + override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService override fun pushersService(): PushersService = pushersService diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryList.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryList.kt new file mode 100644 index 0000000000..b01501d328 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryList.kt @@ -0,0 +1,31 @@ +/* + * 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.test.roomdirectory + +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +class FakeRoomDirectoryList( + override val state: Flow = emptyFlow(), + val filterLambda: (String?, Int) -> Result = { _, _ -> Result.success(Unit) }, + val loadMoreLambda: () -> Result = { Result.success(Unit) } +) : RoomDirectoryList { + override suspend fun filter(filter: String?, batchSize: Int) = filterLambda(filter, batchSize) + + override suspend fun loadMore(): Result = loadMoreLambda() +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt index 25f714cfde..68926b9deb 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt @@ -20,8 +20,8 @@ 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(scope: CoroutineScope): RoomDirectoryList { - TODO("Not yet implemented") - } +class FakeRoomDirectoryService( + private val createRoomDirectoryListFactory: (CoroutineScope) -> RoomDirectoryList = { throw AssertionError("Configure a proper factory.") } +) : RoomDirectoryService { + override fun createRoomDirectoryList(scope: CoroutineScope) = createRoomDirectoryListFactory(scope) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt new file mode 100644 index 0000000000..3e53a0c2e3 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt @@ -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.test.roomdirectory + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription +import io.element.android.libraries.matrix.test.A_ROOM_ID + +fun aRoomDescription( + roomId: RoomId = A_ROOM_ID, + name: String? = null, + topic: String? = null, + alias: String? = null, + avatarUrl: String? = null, + joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN, + isWorldReadable: Boolean = true, + joinedMembers: Long = 2L +) = RoomDescription( + roomId = roomId, + name = name, + topic = topic, + alias = alias, + avatarUrl = avatarUrl, + joinRule = joinRule, + isWorldReadable = isWorldReadable, + joinedMembers = joinedMembers +) diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt index 1d237979e6..4374d77e52 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt @@ -100,4 +100,9 @@ object TestTags { * Timeline item. */ val timelineItemSenderInfo = TestTag("timeline_item-sender_info") + + /** + * Search field. + */ + val searchTextField = TestTag("search_text_field") } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index e651e8f968..827038785c 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -118,7 +118,8 @@ class RoomListScreen( roomListService = matrixClient.roomListService, roomSummaryFactory = roomListRoomSummaryFactory, coroutineDispatchers = coroutineDispatchers, - ) + ), + featureFlagService = featureFlagService, ), sessionPreferencesStore = DefaultSessionPreferencesStore( context = context, @@ -156,6 +157,7 @@ class RoomListScreen( onInvitesClicked = {}, onRoomSettingsClicked = {}, onMenuActionClicked = {}, + onRoomDirectorySearchClicked = {}, modifier = modifier, ) diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/PresenterTest.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/PresenterTest.kt new file mode 100644 index 0000000000..2735827134 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/PresenterTest.kt @@ -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.tests.testutils + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.TurbineTestContext +import app.cash.turbine.test +import io.element.android.libraries.architecture.Presenter +import kotlin.time.Duration + +suspend fun Presenter.test( + timeout: Duration? = null, + name: String? = null, + validate: suspend TurbineTestContext.() -> Unit, +) { + moleculeFlow(RecompositionMode.Immediate) { + present() + }.test(timeout, name, validate) +} From 2103306387d9859015ecdac53e10a7774b03094c Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 28 Mar 2024 16:15:11 +0000 Subject: [PATCH 14/21] Update screenshots --- ..._null_RoomDirectoryView-Day-0_1_null_0,NEXUS_5,1.0,en].png | 3 +++ ..._null_RoomDirectoryView-Day-0_1_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ull_RoomDirectoryView-Night-0_2_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ull_RoomDirectoryView-Night-0_2_null_1,NEXUS_5,1.0,en].png | 3 +++ ...stSearchResultContent-Day-13_14_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...stSearchResultContent-Day-13_14_null_2,NEXUS_5,1.0,en].png | 3 +++ ...SearchResultContent-Night-13_15_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...SearchResultContent-Night-13_15_null_2,NEXUS_5,1.0,en].png | 3 +++ ...r_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png | 3 +++ ...r_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png | 3 +++ ...r_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png | 3 +++ 11 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bd44675e52 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b6110eb2eb66c404f787472dd18608a27a3c8ce19c794ed4b3f67e186720978 +size 13703 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8d0538f792 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2b6cc7995880af654439c4f45d6e3ff35ff9c92609ad136fcdfbb2050edea44 +size 30484 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ab56f08348 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9eafd587065c22d19814285b63e7690f5ff4bd1ed0765bd9802e61d6d8ed0ba7 +size 12915 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1ee85c5f13 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a7b5c11dbc45a804d34a0105c49c7bd15ab1f5d6441f0e95dba84fd8a9eada6 +size 29080 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_1,NEXUS_5,1.0,en].png index 5cc0279077..872a2f2274 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1625ac34428f660c235d1d64f5de867baa6c0ca296f0a93d81588d633d6a74bf -size 30082 +oid sha256:615e00dfa47d40fd459d268d6e68c61a501be7e4a24bfe7d18f3a647da8c5f08 +size 10595 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5cc0279077 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1625ac34428f660c235d1d64f5de867baa6c0ca296f0a93d81588d633d6a74bf +size 30082 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_1,NEXUS_5,1.0,en].png index f7230cc774..845fd9fc0a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:264d9373767b6b59d0cea4bfa4d148a453058be70e7805581fe96d7448ef5232 -size 29978 +oid sha256:421e47de6c2bc7fd0a5de84b6db4bea79c7312442244ec11506164150762943b +size 9787 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f7230cc774 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:264d9373767b6b59d0cea4bfa4d148a453058be70e7805581fe96d7448ef5232 +size 29978 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ec81aa14bc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7c46e25d0b339f237f5c5d98d850664638b2084b3fe4a75e5d7b50d3b678b8b +size 18999 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..27df947568 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:150055f181351f24e22ba29492904b2296edc7c826aaa81952216734ec4431f9 +size 18098 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0f2ea2dda3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a623b5b5bd5700f38b9eb790a37a5b029e1a74b8ae9811f9732dec808cacc770 +size 21231 From 42004f251bbcc260ee8b44aa78c4709e533460e9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 28 Mar 2024 18:07:48 +0100 Subject: [PATCH 15/21] Room directory : more cleanup and more tests --- .../impl/src/main/res/values/localazy.xml | 4 ++ .../impl/root/RoomDirectoryStateProvider.kt | 17 +++++- .../impl/root/RoomDirectoryView.kt | 44 ++++++++------- .../impl/src/main/res/values/localazy.xml | 5 ++ .../impl/search/RoomListSearchView.kt | 11 ++-- .../src/main/res/values/localazy.xml | 53 ++++++++++++++++++- tools/localazy/config.json | 6 +++ 7 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 features/roomdirectory/impl/src/main/res/values/localazy.xml diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index cde723257f..b6bf76c440 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -29,7 +29,11 @@ "Demote" "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges." "Demote yourself?" + "%1$s (Pending)" "Edit Moderators" + "Admins" + "Moderators" + "Members" "You have unsaved changes." "Save changes?" "Add topic" diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt index 3fa7877b6f..efb6624260 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt @@ -32,7 +32,22 @@ open class RoomDirectoryStateProvider : PreviewParameterProvider + + "Failed loading" + "Room directory" + diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt index 53528e0558..80657ed4fc 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.roomlist.impl.R import io.element.android.features.roomlist.impl.components.RoomSummaryRow import io.element.android.features.roomlist.impl.contentType import io.element.android.features.roomlist.impl.model.RoomListRoomSummary @@ -131,8 +132,8 @@ private fun RoomListSearchContent( val focusRequester = FocusRequester() TextField( modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), + .fillMaxWidth() + .focusRequester(focusRequester), value = filter, singleLine = true, onValueChange = { state.eventSink(RoomListSearchEvents.QueryChanged(it)) }, @@ -171,8 +172,8 @@ private fun RoomListSearchContent( ) { padding -> Column( modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) + .padding(padding) + .consumeWindowInsets(padding) ) { if (state.displayRoomDirectorySearch) { RoomDirectorySearchButton( @@ -206,7 +207,7 @@ private fun RoomDirectorySearchButton( modifier: Modifier = Modifier ) { Button( - text = "Browse all rooms", + text = stringResource(id = R.string.screen_roomlist_room_directory_button_title), leadingIcon = IconSource.Vector(CompoundIcons.ListBulleted()), onClick = onClick, modifier = modifier, diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 97ddec4b3b..1bc20364ce 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -254,8 +254,6 @@ "Failed selecting media, please try again." "Failed processing media to upload, please try again." "Failed uploading media, please try again." - "Failed loading" - "Room directory" "Failed processing media to upload, please try again." "Could not retrieve user details" "Block" @@ -274,4 +272,55 @@ "Version: %1$s (%2$s)" "en" "en" + "Troubleshoot" + "Troubleshoot notifications" + "Run tests" + "Run tests again" + "Some tests failed. Please check the details." + "Run the tests to detect any issue in your configuration that may make notifications not behave as expected." + "Attempt to fix" + "All tests passed successfully." + "Troubleshoot notifications" + "Some tests require your attention. Please check the details." + "Check that the application can show notifications." + "Check permissions" + "Get the name of the current provider." + "No push providers selected." + "Current push provider: %1$s." + "Current push provider" + "Ensure that the application has at least one push provider." + "No push providers found." + + "Found %1$d push provider: %2$s" + "Found %1$d push providers: %2$s" + + "Detect push providers" + "Check that the application can display notification." + "The notification has not been clicked." + "Cannot display the notification." + "The notification has been clicked!" + "Display notification" + "Please click on the notification to continue the test." + "Ensure that Firebase is available." + "Firebase is not available." + "Firebase is available." + "Check Firebase" + "Ensure that Firebase token is available." + "Firebase token is not known." + "Firebase token: %1$s." + "Check Firebase token" + "Ensure that the application is receiving push." + "Error: pusher has rejected the request." + "Error: %1$s." + "Error, cannot test push." + "Error, timeout waiting for push." + "Push loop back took %1$d ms." + "Test Push loop back" + "Ensure that UnifiedPush distributors are available." + "No push distributors found." + + "%1$d distributor found: %2$s." + "%1$d distributors found: %2$s." + + "Check UnifiedPush" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index b3ad636c86..aaa1f6734b 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -197,6 +197,12 @@ "screen_app_lock_.*", "screen_signout_in_progress_dialog_content" ] + }, + { + "name" : ":features:roomdirectory:impl", + "includeRegex" : [ + "screen_room_directory_.*" + ] } ] } From d94f4b0392750cda411b0bf4f0450db43fa1a600 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 28 Mar 2024 17:25:55 +0000 Subject: [PATCH 16/21] Update screenshots --- ...w_null_RoomDirectoryView-Day-0_1_null_2,NEXUS_5,1.0,en].png | 3 +++ ...w_null_RoomDirectoryView-Day-0_1_null_3,NEXUS_5,1.0,en].png | 3 +++ ...w_null_RoomDirectoryView-Day-0_1_null_4,NEXUS_5,1.0,en].png | 3 +++ ...null_RoomDirectoryView-Night-0_2_null_2,NEXUS_5,1.0,en].png | 3 +++ ...null_RoomDirectoryView-Night-0_2_null_3,NEXUS_5,1.0,en].png | 3 +++ ...null_RoomDirectoryView-Night-0_2_null_4,NEXUS_5,1.0,en].png | 3 +++ 6 files changed, 18 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..158c2f7cf5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f7885c0ee25d12dc5bb25940b35e7153f95ffc35cbe6fac21839390d52faeac +size 32326 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c77504c186 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:776d8f8a15bac930896a8baaf46f69fddf24fdf5c51c1ab6f1bbc48ef89259d6 +size 31607 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0eb807db6d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b349aa0ce78db6396094adda401f52133c32c16e695a051ba115be0143c31de9 +size 34972 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..80d7eb5d17 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6af6d92766e7249ee95582a32ac17ddd5c3bcc29c9b78d9ba9ca6bcb27b69c8c +size 30828 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..34fa07061a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b434b666666da81963d8ac89e9669f059443cd529103735944e6a15ba5ba46b5 +size 28646 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cbf54de729 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0190f4e7d06f5b46147531d7224e24662e0a7ef6289d516cf082d59ab63627f +size 31342 From 5f8b74055d698ed86525061c56c7bacce9a741a0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 29 Mar 2024 11:37:00 +0100 Subject: [PATCH 17/21] RoomDirectory : address pr reviews --- features/roomdirectory/impl/build.gradle.kts | 2 -- .../roomdirectory/impl/root/RoomDirectoryPresenter.kt | 4 ++-- .../element/android/libraries/featureflag/api/FeatureFlags.kt | 2 +- .../libraries/matrix/api/roomdirectory/RoomDescription.kt | 2 +- .../matrix/impl/roomdirectory/RoomDescriptionMapper.kt | 2 +- .../matrix/test/roomdirectory/RoomDescriptionFixture.kt | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/features/roomdirectory/impl/build.gradle.kts b/features/roomdirectory/impl/build.gradle.kts index d26a75ca8c..85bb195da3 100644 --- a/features/roomdirectory/impl/build.gradle.kts +++ b/features/roomdirectory/impl/build.gradle.kts @@ -14,8 +14,6 @@ * limitations under the License. */ -// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed -@Suppress("DSL_SCOPE_VIOLATION") plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt index 95cfa8166b..5d4cef55cb 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt @@ -67,10 +67,10 @@ class RoomDirectoryPresenter @Inject constructor( } LaunchedEffect(searchQuery) { if (searchQuery == null) return@LaunchedEffect - // debounce search query - delay(300) // cancel load more right away loadingMore = false + // debounce search query + delay(300) roomDirectoryList.filter(searchQuery, 20) } LaunchedEffect(loadingMore) { diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 7140a89064..3a52e63b52 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -92,7 +92,7 @@ enum class FeatureFlags( RoomDirectorySearch( key = "feature.roomdirectorysearch", title = "Room directory search", - description = "Allow user to search for public rooms in his homeserver", + description = "Allow user to search for public rooms in their homeserver", defaultValue = true, isFinished = false, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDescription.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDescription.kt index dddd556bbe..78d6cb0c94 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDescription.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDescription.kt @@ -26,7 +26,7 @@ data class RoomDescription( val avatarUrl: String?, val joinRule: JoinRule, val isWorldReadable: Boolean, - val joinedMembers: Long + val numberOfMembers: Long ) { enum class JoinRule { PUBLIC, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt index 91b84e48c0..876a58d3a5 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt @@ -35,7 +35,7 @@ class RoomDescriptionMapper { null -> RoomDescription.JoinRule.UNKNOWN }, isWorldReadable = roomDescription.isWorldReadable, - joinedMembers = roomDescription.joinedMembers.toLong(), + numberOfMembers = roomDescription.joinedMembers.toLong(), ) } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt index 3e53a0c2e3..6e96ca4452 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt @@ -37,5 +37,5 @@ fun aRoomDescription( avatarUrl = avatarUrl, joinRule = joinRule, isWorldReadable = isWorldReadable, - joinedMembers = joinedMembers + numberOfMembers = joinedMembers ) From d34c4605a41cad1008d8af7fa5ef4983a2f591f8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 29 Mar 2024 11:37:29 +0100 Subject: [PATCH 18/21] RoomDirectory : use same logic for name description computation --- .../impl/root/model/RoomDescription.kt | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt index 405d4241b2..be36eb5053 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt @@ -22,13 +22,29 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription as MatrixRoomDescription fun MatrixRoomDescription.toFeatureModel(): RoomDescription { + fun name(): String { + return name ?: alias ?: roomId.value + } + + fun description(): String { + val topic = topic + val alias = alias + val name = name + return when { + topic != null -> topic + name != null && alias != null -> alias + name == null && alias == null -> "" + else -> roomId.value + } + } + return RoomDescription( roomId = roomId, - name = name ?: "", - description = topic ?: alias ?: roomId.value, + name = name(), + description = description(), avatarData = AvatarData( id = roomId.value, - name = name ?: "", + name = name, url = avatarUrl, size = AvatarSize.RoomDirectoryItem, ), From 11dfb32f72f13094ebafdf76ab88d825e88281d2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 29 Mar 2024 11:37:36 +0100 Subject: [PATCH 19/21] Room directory : fix ci --- libraries/ui-strings/src/main/res/values-be/translations.xml | 2 -- libraries/ui-strings/src/main/res/values-cs/translations.xml | 2 -- libraries/ui-strings/src/main/res/values-de/translations.xml | 2 -- libraries/ui-strings/src/main/res/values-fr/translations.xml | 2 -- libraries/ui-strings/src/main/res/values-ru/translations.xml | 2 -- libraries/ui-strings/src/main/res/values-sk/translations.xml | 2 -- libraries/ui-strings/src/main/res/values-uk/translations.xml | 2 -- 7 files changed, 14 deletions(-) diff --git a/libraries/ui-strings/src/main/res/values-be/translations.xml b/libraries/ui-strings/src/main/res/values-be/translations.xml index f163de2540..70641271b8 100644 --- a/libraries/ui-strings/src/main/res/values-be/translations.xml +++ b/libraries/ui-strings/src/main/res/values-be/translations.xml @@ -258,8 +258,6 @@ "Не ўдалося выбраць носьбіт, паўтарыце спробу." "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз." "Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз." - "Памылка загрузкі" - "Каталог пакояў" "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз." "Не ўдалося атрымаць інфармацыю пра карыстальніка" "Заблакіраваць" diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index 6975c64660..9dde757dc1 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -258,8 +258,6 @@ "Výběr média se nezdařil, zkuste to prosím znovu." "Nahrání média se nezdařilo, zkuste to prosím znovu." "Nahrání média se nezdařilo, zkuste to prosím znovu." - "Načítání se nezdařilo" - "Adresář místností" "Nahrání média se nezdařilo, zkuste to prosím znovu." "Nepodařilo se načíst údaje o uživateli" "Zablokovat" diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 83b3dcac0a..8a3e035833 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -254,8 +254,6 @@ "Medienauswahl fehlgeschlagen, bitte versuche es erneut." "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." "Das Hochladen der Medien ist fehlgeschlagen. Bitte versuche es erneut." - "Fehler beim Laden" - "Raumverzeichnis" "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." "Benutzerdetails konnten nicht abgerufen werden" "Blockieren" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 219b8501c5..e3b6e3d0d9 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -254,8 +254,6 @@ "Échec de la sélection du média, veuillez réessayer." "Échec du traitement des médias à télécharger, veuillez réessayer." "Échec du téléchargement du média, veuillez réessayer." - "Échec du chargement" - "Annuaire des salons" "Échec du traitement des médias à télécharger, veuillez réessayer." "Impossible de récupérer les détails de l’utilisateur" "Bloquer" diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index 01a23b4c24..7758a05eeb 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -256,8 +256,6 @@ "Не удалось выбрать носитель, попробуйте еще раз." "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." "Не удалось загрузить медиафайлы, попробуйте еще раз." - "Сбой загрузки" - "Каталог комнат" "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." "Не удалось получить данные о пользователе" "Заблокировать" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 9c1df72eaf..f49201bda1 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -257,8 +257,6 @@ "Nepodarilo sa vybrať médium, skúste to prosím znova." "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." "Nepodarilo sa nahrať médiá, skúste to prosím znova." - "Načítanie zlyhalo" - "Adresár miestností" "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." "Nepodarilo sa získať údaje o používateľovi" "Zablokovať" diff --git a/libraries/ui-strings/src/main/res/values-uk/translations.xml b/libraries/ui-strings/src/main/res/values-uk/translations.xml index 542ff641aa..a97f92401a 100644 --- a/libraries/ui-strings/src/main/res/values-uk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -256,8 +256,6 @@ "Не вдалося вибрати медіафайл, спробуйте ще раз." "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз." "Не вдалося завантажити медіафайл, спробуйте ще раз." - "Не вдалося завантажити" - "Каталог кімнат" "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз." "Не вдалося отримати дані користувача" "Заблокувати" From ff9603f33871cdef025453d0b96cd3f246030d60 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 29 Mar 2024 16:17:49 +0100 Subject: [PATCH 20/21] RoomDirectory : fix maestro (search screen is closed automatically) --- .maestro/tests/roomList/searchRoomList.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.maestro/tests/roomList/searchRoomList.yaml b/.maestro/tests/roomList/searchRoomList.yaml index 8b41c4d259..5d5e01de84 100644 --- a/.maestro/tests/roomList/searchRoomList.yaml +++ b/.maestro/tests/roomList/searchRoomList.yaml @@ -7,8 +7,4 @@ appId: ${MAESTRO_APP_ID} - tapOn: ${MAESTRO_ROOM_NAME} # Back from timeline - back -- assertVisible: "MyR" -- hideKeyboard -# Back from search -- back - runFlow: ../assertions/assertHomeDisplayed.yaml From 83255831337d31555104ccf3a20b376cdf59fd09 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 29 Mar 2024 16:17:57 +0100 Subject: [PATCH 21/21] RoomDirectory : set default value of FeatureFlag to false. --- .../element/android/libraries/featureflag/api/FeatureFlags.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 3a52e63b52..5642eefbd2 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -93,7 +93,7 @@ enum class FeatureFlags( key = "feature.roomdirectorysearch", title = "Room directory search", description = "Allow user to search for public rooms in their homeserver", - defaultValue = true, + defaultValue = false, isFinished = false, ) }