Merge pull request #2620 from element-hq/feature/fga/room_directory
Feature/fga/room directory
This commit is contained in:
commit
a9f326c81c
78 changed files with 2114 additions and 41 deletions
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.22" />
|
||||
<option name="version" value="1.9.23" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<LoggedInFlowNode.NavTarget>(
|
||||
|
|
@ -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<Callback>().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 onOpenRoom(roomId: RoomId) {
|
||||
coroutineScope.launch { attachRoom(roomId) }
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,11 @@
|
|||
<string name="screen_room_change_role_confirm_demote_self_action">"Demote"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"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."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Demote yourself?"</string>
|
||||
<string name="screen_room_change_role_invited_member_name">"%1$s (Pending)"</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Edit Moderators"</string>
|
||||
<string name="screen_room_change_role_section_administrators">"Admins"</string>
|
||||
<string name="screen_room_change_role_section_moderators">"Moderators"</string>
|
||||
<string name="screen_room_change_role_section_users">"Members"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"You have unsaved changes."</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Save changes?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Add topic"</string>
|
||||
|
|
|
|||
29
features/roomdirectory/api/build.gradle.kts
Normal file
29
features/roomdirectory/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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)
|
||||
implementation(projects.libraries.designsystem)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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.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 onOpenRoom(roomId: RoomId)
|
||||
}
|
||||
}
|
||||
60
features/roomdirectory/impl/build.gradle.kts
Normal file
60
features/roomdirectory/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.ksp)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.roomdirectory.impl"
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
|
@ -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.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.features.roomdirectory.impl.root.RoomDirectoryNode
|
||||
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<Plugin>()
|
||||
|
||||
return object : RoomDirectoryEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: RoomDirectoryEntryPoint.Callback): RoomDirectoryEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<RoomDirectoryNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.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
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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.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.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(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RoomDirectoryPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private fun onRoomJoined(roomId: RoomId) {
|
||||
plugins<RoomDirectoryEntryPoint.Callback>().forEach {
|
||||
it.onOpenRoom(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
RoomDirectoryView(
|
||||
state = state,
|
||||
onRoomJoined = ::onRoomJoined,
|
||||
onBackPressed = ::navigateUp,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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.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
|
||||
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.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
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
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomDirectoryPresenter @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val joinRoom: JoinRoom,
|
||||
private val roomDirectoryService: RoomDirectoryService,
|
||||
) : Presenter<RoomDirectoryState> {
|
||||
@Composable
|
||||
override fun present(): RoomDirectoryState {
|
||||
var loadingMore by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var searchQuery by rememberSaveable {
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val roomDirectoryList = remember {
|
||||
roomDirectoryService.createRoomDirectoryList(coroutineScope)
|
||||
}
|
||||
val listState by roomDirectoryList.collectState()
|
||||
val joinRoomAction: MutableState<AsyncAction<RoomId>> = remember {
|
||||
mutableStateOf(AsyncAction.Uninitialized)
|
||||
}
|
||||
LaunchedEffect(searchQuery) {
|
||||
if (searchQuery == null) return@LaunchedEffect
|
||||
// cancel load more right away
|
||||
loadingMore = false
|
||||
// debounce search query
|
||||
delay(300)
|
||||
roomDirectoryList.filter(searchQuery, 20)
|
||||
}
|
||||
LaunchedEffect(loadingMore) {
|
||||
if (loadingMore) {
|
||||
roomDirectoryList.loadMore()
|
||||
loadingMore = false
|
||||
}
|
||||
}
|
||||
fun handleEvents(event: RoomDirectoryEvents) {
|
||||
when (event) {
|
||||
RoomDirectoryEvents.LoadMore -> {
|
||||
loadingMore = true
|
||||
}
|
||||
is RoomDirectoryEvents.Search -> {
|
||||
searchQuery = event.query
|
||||
}
|
||||
is RoomDirectoryEvents.JoinRoom -> {
|
||||
coroutineScope.joinRoom(joinRoomAction, event.roomId)
|
||||
}
|
||||
RoomDirectoryEvents.JoinRoomDismissError -> {
|
||||
joinRoomAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RoomDirectoryState(
|
||||
query = searchQuery.orEmpty(),
|
||||
roomDescriptions = listState.items,
|
||||
displayLoadMoreIndicator = listState.hasMoreToLoad,
|
||||
joinRoomAction = joinRoomAction.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.joinRoom(state: MutableState<AsyncAction<RoomId>>, roomId: RoomId) = launch {
|
||||
state.runUpdatingState {
|
||||
joinRoom(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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(RoomDirectoryListState.Default)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
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<RoomDescription>,
|
||||
val displayLoadMoreIndicator: Boolean,
|
||||
val joinRoomAction: AsyncAction<RoomId>,
|
||||
val eventSink: (RoomDirectoryEvents) -> Unit
|
||||
) {
|
||||
val displayEmptyState = roomDescriptions.isEmpty() && !displayLoadMoreIndicator
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
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
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class RoomDirectoryStateProvider : PreviewParameterProvider<RoomDirectoryState> {
|
||||
override val values: Sequence<RoomDirectoryState>
|
||||
get() = sequenceOf(
|
||||
aRoomDirectoryState(),
|
||||
aRoomDirectoryState(
|
||||
query = "Element",
|
||||
roomDescriptions = aRoomDescriptionList(),
|
||||
),
|
||||
aRoomDirectoryState(
|
||||
query = "Element",
|
||||
roomDescriptions = aRoomDescriptionList(),
|
||||
displayLoadMoreIndicator = true,
|
||||
),
|
||||
aRoomDirectoryState(
|
||||
query = "Element",
|
||||
roomDescriptions = aRoomDescriptionList(),
|
||||
joinRoomAction = AsyncAction.Loading,
|
||||
),
|
||||
aRoomDirectoryState(
|
||||
query = "Element",
|
||||
roomDescriptions = aRoomDescriptionList(),
|
||||
joinRoomAction = AsyncAction.Failure(Exception("Failed to join room")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aRoomDirectoryState(
|
||||
query: String = "",
|
||||
displayLoadMoreIndicator: Boolean = false,
|
||||
roomDescriptions: ImmutableList<RoomDescription> = persistentListOf(),
|
||||
joinRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
|
||||
eventSink: (RoomDirectoryEvents) -> Unit = {},
|
||||
) = RoomDirectoryState(
|
||||
query = query,
|
||||
roomDescriptions = roomDescriptions,
|
||||
displayLoadMoreIndicator = displayLoadMoreIndicator,
|
||||
joinRoomAction = joinRoomAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
fun aRoomDescriptionList(): ImmutableList<RoomDescription> {
|
||||
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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,321 @@
|
|||
/*
|
||||
* 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.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
|
||||
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.layout.wrapContentHeight
|
||||
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.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
|
||||
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.api.RoomDescription
|
||||
import io.element.android.features.roomdirectory.impl.R
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun RoomDirectoryView(
|
||||
state: RoomDirectoryState,
|
||||
onRoomJoined: (RoomId) -> Unit,
|
||||
onBackPressed: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun joinRoom(roomId: RoomId) {
|
||||
state.eventSink(RoomDirectoryEvents.JoinRoom(roomId))
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
RoomDirectoryTopBar(onBackPressed = onBackPressed)
|
||||
},
|
||||
content = { padding ->
|
||||
RoomDirectoryContent(
|
||||
state = state,
|
||||
onResultClicked = ::joinRoom,
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
)
|
||||
}
|
||||
)
|
||||
AsyncActionView(
|
||||
async = state.joinRoomAction,
|
||||
onSuccess = onRoomJoined,
|
||||
onErrorDismiss = {
|
||||
state.eventSink(RoomDirectoryEvents.JoinRoomDismissError)
|
||||
},
|
||||
errorMessage = {
|
||||
stringResource(id = CommonStrings.error_unknown)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun RoomDirectoryTopBar(
|
||||
onBackPressed: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackPressed)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_room_directory_search_title),
|
||||
style = ElementTheme.typography.aliasScreenTitle,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomDirectoryContent(
|
||||
state: RoomDirectoryState,
|
||||
onResultClicked: (RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
SearchTextField(
|
||||
query = state.query,
|
||||
onQueryChange = { state.eventSink(RoomDirectoryEvents.Search(it)) },
|
||||
placeholder = stringResource(id = CommonStrings.action_search),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
RoomDirectoryRoomList(
|
||||
roomDescriptions = state.roomDescriptions,
|
||||
displayLoadMoreIndicator = state.displayLoadMoreIndicator,
|
||||
displayEmptyState = state.displayEmptyState,
|
||||
onResultClicked = onResultClicked,
|
||||
onReachedLoadMore = { state.eventSink(RoomDirectoryEvents.LoadMore) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomDirectoryRoomList(
|
||||
roomDescriptions: ImmutableList<RoomDescription>,
|
||||
displayLoadMoreIndicator: Boolean,
|
||||
displayEmptyState: Boolean,
|
||||
onResultClicked: (RoomId) -> Unit,
|
||||
onReachedLoadMore: () -> Unit,
|
||||
modifier: 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(onReachedLoadMore) {
|
||||
onReachedLoadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadMoreIndicator(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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.testTag(TestTags.searchTextField.value),
|
||||
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(
|
||||
roomDescription: RoomDescription,
|
||||
onClick: (RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = roomDescription.canBeJoined) {
|
||||
onClick(roomDescription.roomId)
|
||||
}
|
||||
.padding(
|
||||
top = 12.dp,
|
||||
bottom = 12.dp,
|
||||
start = 16.dp,
|
||||
)
|
||||
.height(IntrinsicSize.Min),
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = roomDescription.avatarData,
|
||||
modifier = Modifier.align(Alignment.CenterVertically)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = roomDescription.name,
|
||||
maxLines = 1,
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = roomDescription.description,
|
||||
maxLines = 1,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
if (roomDescription.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview {
|
||||
RoomDirectoryView(
|
||||
state = state,
|
||||
onRoomJoined = {},
|
||||
onBackPressed = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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<RoomId>
|
||||
}
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultJoinRoom @Inject constructor(private val client: MatrixClient) : JoinRoom {
|
||||
override suspend fun invoke(roomId: RoomId) = client.joinRoom(roomId)
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
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 = description(),
|
||||
avatarData = AvatarData(
|
||||
id = roomId.value,
|
||||
name = name,
|
||||
url = avatarUrl,
|
||||
size = AvatarSize.RoomDirectoryItem,
|
||||
),
|
||||
canBeJoined = joinRule == MatrixRoomDescription.JoinRule.PUBLIC,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomdirectory.impl.root.model
|
||||
|
||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal data class RoomDirectoryListState(
|
||||
val hasMoreToLoad: Boolean,
|
||||
val items: ImmutableList<RoomDescription>,
|
||||
) {
|
||||
companion object {
|
||||
val Default = RoomDirectoryListState(
|
||||
hasMoreToLoad = true,
|
||||
items = persistentListOf()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_directory_search_loading_error">"Failed loading"</string>
|
||||
<string name="screen_room_directory_search_title">"Room directory"</string>
|
||||
</resources>
|
||||
|
|
@ -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<RoomId> = { Result.success(it) }
|
||||
) : JoinRoom {
|
||||
override suspend fun invoke(roomId: RoomId) = lambda(roomId)
|
||||
}
|
||||
|
|
@ -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<RoomDirectoryList.State>(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<RoomDirectoryList.State>(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<RoomId>(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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `typing text in search field emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
|
||||
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<RoomDirectoryEvents>()
|
||||
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<RoomDirectoryEvents>()
|
||||
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<RoomDirectoryEvents>(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 <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDirectoryView(
|
||||
state: RoomDirectoryState,
|
||||
onBackPressed: () -> Unit = EnsureNeverCalled(),
|
||||
onRoomJoined: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
) {
|
||||
setContent {
|
||||
RoomDirectoryView(
|
||||
state = state,
|
||||
onRoomJoined = onRoomJoined,
|
||||
onBackPressed = onBackPressed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,5 +38,6 @@ interface RoomListEntryPoint : FeatureEntryPoint {
|
|||
fun onInvitesClicked()
|
||||
fun onRoomSettingsClicked(roomId: RoomId)
|
||||
fun onReportBugClicked()
|
||||
fun onRoomDirectorySearchClicked()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ class RoomListNode @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onRoomDirectorySearchClicked() {
|
||||
plugins<RoomListEntryPoint.Callback>().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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,21 +21,25 @@ 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 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<RoomListSearchState> {
|
||||
@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("")
|
||||
}
|
||||
|
||||
|
|
@ -62,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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,5 +23,8 @@ data class RoomListSearchState(
|
|||
val isSearchActive: Boolean,
|
||||
val query: String,
|
||||
val results: ImmutableList<RoomListRoomSummary>,
|
||||
val isRoomDirectorySearchEnabled: Boolean,
|
||||
val eventSink: (RoomListSearchEvents) -> Unit
|
||||
)
|
||||
) {
|
||||
val displayRoomDirectorySearch = query.isEmpty() && isRoomDirectorySearchEnabled
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class RoomListSearchStateProvider : PreviewParameterProvider<RoomListSearchState
|
|||
override val values: Sequence<RoomListSearchState>
|
||||
get() = sequenceOf(
|
||||
aRoomListSearchState(),
|
||||
aRoomListSearchState(isRoomDirectorySearchEnabled = true),
|
||||
aRoomListSearchState(
|
||||
isSearchActive = true,
|
||||
query = "Test",
|
||||
|
|
@ -38,10 +39,12 @@ fun aRoomListSearchState(
|
|||
isSearchActive: Boolean = false,
|
||||
query: String = "",
|
||||
results: ImmutableList<RoomListRoomSummary> = persistentListOf(),
|
||||
isRoomDirectorySearchEnabled: Boolean = false,
|
||||
eventSink: (RoomListSearchEvents) -> Unit = { },
|
||||
) = RoomListSearchState(
|
||||
isSearchActive = isSearchActive,
|
||||
query = query,
|
||||
results = results,
|
||||
isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -50,8 +51,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 +67,7 @@ internal fun RoomListSearchView(
|
|||
state: RoomListSearchState,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
onRoomDirectorySearchClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BackHandler(enabled = state.isSearchActive) {
|
||||
|
|
@ -87,6 +91,7 @@ internal fun RoomListSearchView(
|
|||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = onRoomLongClicked,
|
||||
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -99,6 +104,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 +175,14 @@ private fun RoomListSearchContent(
|
|||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
if (state.displayRoomDirectorySearch) {
|
||||
RoomDirectorySearchButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp),
|
||||
onClick = onRoomDirectorySearchClicked
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
|
|
@ -187,12 +201,26 @@ private fun RoomListSearchContent(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomDirectorySearchButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Button(
|
||||
text = stringResource(id = R.string.screen_roomlist_room_directory_button_title),
|
||||
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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
|||
onInvitesClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onRoomDirectorySearchClicked: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
RoomListView(
|
||||
|
|
@ -203,6 +204,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
|||
onInvitesClicked = onInvitesClicked,
|
||||
onRoomSettingsClicked = onRoomSettingsClicked,
|
||||
onMenuActionClicked = onMenuActionClicked,
|
||||
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,7 @@ enum class AvatarSize(val dp: Dp) {
|
|||
|
||||
NotificationsOptIn(32.dp),
|
||||
|
||||
CustomRoomNotificationSetting(36.dp)
|
||||
CustomRoomNotificationSetting(36.dp),
|
||||
|
||||
RoomDirectoryItem(36.dp),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 their homeserver",
|
||||
defaultValue = false,
|
||||
isFinished = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
|
|||
FeatureFlags.MarkAsUnread -> true
|
||||
FeatureFlags.RoomListFilters -> true
|
||||
FeatureFlags.RoomModeration -> false
|
||||
FeatureFlags.RoomDirectorySearch -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -58,12 +59,14 @@ interface MatrixClient : Closeable {
|
|||
suspend fun setDisplayName(displayName: String): Result<Unit>
|
||||
suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result<Unit>
|
||||
suspend fun removeAvatar(): Result<Unit>
|
||||
suspend fun joinRoom(roomId: RoomId): Result<RoomId>
|
||||
fun syncService(): SyncService
|
||||
fun sessionVerificationService(): SessionVerificationService
|
||||
fun pushersService(): PushersService
|
||||
fun notificationService(): NotificationService
|
||||
fun notificationSettingsService(): NotificationSettingsService
|
||||
fun encryptionService(): EncryptionService
|
||||
fun roomDirectoryService(): RoomDirectoryService
|
||||
suspend fun getCacheSize(): Long
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 numberOfMembers: Long
|
||||
) {
|
||||
enum class JoinRule {
|
||||
PUBLIC,
|
||||
KNOCK,
|
||||
UNKNOWN
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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.Flow
|
||||
|
||||
interface RoomDirectoryList {
|
||||
suspend fun filter(filter: String?, batchSize: Int): Result<Unit>
|
||||
suspend fun loadMore(): Result<Unit>
|
||||
val state: Flow<State>
|
||||
|
||||
data class State(
|
||||
val hasMoreToLoad: Boolean,
|
||||
val items: List<RoomDescription>,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.CoroutineScope
|
||||
|
||||
interface RoomDirectoryService {
|
||||
fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
|
|||
import io.element.android.libraries.matrix.api.pusher.PushersService
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
|
|
@ -53,6 +54,7 @@ import io.element.android.libraries.matrix.impl.room.MatrixRoomInfoMapper
|
|||
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
|
||||
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
||||
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
|
||||
import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService
|
||||
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
|
||||
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
|
||||
import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline
|
||||
|
|
@ -71,6 +73,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -101,6 +104,8 @@ import org.matrix.rustcomponents.sdk.use
|
|||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters
|
||||
import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset
|
||||
import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility
|
||||
|
|
@ -150,6 +155,12 @@ class RustMatrixClient(
|
|||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
private val roomDirectoryService = RustRoomDirectoryService(
|
||||
client = client,
|
||||
sessionDispatcher = sessionDispatcher,
|
||||
)
|
||||
|
||||
private val sessionDirectoryNameProvider = SessionDirectoryNameProvider()
|
||||
|
||||
private val isLoggingOut = AtomicBoolean(false)
|
||||
|
|
@ -309,6 +320,22 @@ class RustMatrixClient(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the room to be available in the room list.
|
||||
* @param roomId the room id to wait for
|
||||
* @param timeout the timeout to wait for the room to be available
|
||||
* @throws TimeoutCancellationException if the room is not available after the timeout
|
||||
*/
|
||||
private suspend fun awaitRoom(roomId: RoomId, timeout: Duration) {
|
||||
withTimeout(timeout) {
|
||||
roomListService.allRooms.summaries
|
||||
.filter { roomSummaries ->
|
||||
roomSummaries.map { it.identifier() }.contains(roomId.value)
|
||||
}
|
||||
.first()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? {
|
||||
val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value)
|
||||
val fullRoom = cachedRoomListItem?.fullRoomWithTimeline(filter = eventFilters)
|
||||
|
|
@ -358,14 +385,11 @@ class RustMatrixClient(
|
|||
powerLevelContentOverride = defaultRoomCreationPowerLevels,
|
||||
)
|
||||
val roomId = RoomId(client.createRoom(rustParams))
|
||||
|
||||
// Wait to receive the room back from the sync
|
||||
withTimeout(30_000L) {
|
||||
roomListService.allRooms.summaries
|
||||
.filter { roomSummaries ->
|
||||
roomSummaries.map { it.identifier() }.contains(roomId.value)
|
||||
}
|
||||
.first()
|
||||
// Wait to receive the room back from the sync but do not returns failure if it fails.
|
||||
try {
|
||||
awaitRoom(roomId, 30.seconds)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||
}
|
||||
roomId
|
||||
}
|
||||
|
|
@ -414,6 +438,18 @@ class RustMatrixClient(
|
|||
runCatching { client.removeAvatar() }
|
||||
}
|
||||
|
||||
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = withContext(sessionDispatcher) {
|
||||
runCatching {
|
||||
client.joinRoomById(roomId.value).destroy()
|
||||
try {
|
||||
awaitRoom(roomId, 10.seconds)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Timeout waiting for the room to be available in the room list")
|
||||
}
|
||||
roomId
|
||||
}
|
||||
}
|
||||
|
||||
override fun syncService(): SyncService = rustSyncService
|
||||
|
||||
override fun sessionVerificationService(): SessionVerificationService = verificationService
|
||||
|
|
@ -426,6 +462,8 @@ class RustMatrixClient(
|
|||
|
||||
override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService
|
||||
|
||||
override fun roomDirectoryService(): RoomDirectoryService = roomDirectoryService
|
||||
|
||||
override fun close() {
|
||||
sessionCoroutineScope.cancel()
|
||||
clientDelegateTaskHandle?.cancelAndDestroy()
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
|||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -68,4 +69,9 @@ object SessionMatrixModule {
|
|||
fun provideSessionCoroutineScope(matrixClient: MatrixClient): CoroutineScope {
|
||||
return matrixClient.sessionCoroutineScope
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesRoomDirectoryService(matrixClient: MatrixClient): RoomDirectoryService {
|
||||
return matrixClient.roomDirectoryService()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.roomdirectory
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
|
||||
import org.matrix.rustcomponents.sdk.PublicRoomJoinRule
|
||||
import org.matrix.rustcomponents.sdk.RoomDescription as RustRoomDescription
|
||||
|
||||
class RoomDescriptionMapper {
|
||||
fun map(roomDescription: RustRoomDescription): RoomDescription {
|
||||
return RoomDescription(
|
||||
roomId = RoomId(roomDescription.roomId),
|
||||
name = roomDescription.name,
|
||||
topic = roomDescription.topic,
|
||||
avatarUrl = roomDescription.avatarUrl,
|
||||
alias = roomDescription.alias,
|
||||
joinRule = when (roomDescription.joinRule) {
|
||||
PublicRoomJoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC
|
||||
PublicRoomJoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK
|
||||
null -> RoomDescription.JoinRule.UNKNOWN
|
||||
},
|
||||
isWorldReadable = roomDescription.isWorldReadable,
|
||||
numberOfMembers = roomDescription.joinedMembers.toLong(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.roomdirectory
|
||||
|
||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import org.matrix.rustcomponents.sdk.RoomDirectorySearch
|
||||
import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntriesListener
|
||||
import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate
|
||||
import timber.log.Timber
|
||||
|
||||
internal fun RoomDirectorySearch.resultsFlow(): Flow<List<RoomDirectorySearchEntryUpdate>> =
|
||||
callbackFlow {
|
||||
val listener = object : RoomDirectorySearchEntriesListener {
|
||||
override fun onUpdate(roomEntriesUpdate: List<RoomDirectorySearchEntryUpdate>) {
|
||||
trySendBlocking(roomEntriesUpdate)
|
||||
}
|
||||
}
|
||||
val result = results(listener)
|
||||
awaitClose {
|
||||
result.cancelAndDestroy()
|
||||
}
|
||||
}.catch {
|
||||
Timber.d(it, "timelineDiffFlow() failed")
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.roomdirectory
|
||||
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class RoomDirectorySearchProcessor(
|
||||
private val roomDescriptions: MutableSharedFlow<List<RoomDescription>>,
|
||||
private val coroutineContext: CoroutineContext,
|
||||
private val roomDescriptionMapper: RoomDescriptionMapper,
|
||||
) {
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun postUpdates(updates: List<RoomDirectorySearchEntryUpdate>) {
|
||||
updateRoomDescriptions {
|
||||
Timber.v("Update room descriptions from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}")
|
||||
updates.forEach { update ->
|
||||
applyUpdate(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<RoomDescription>.applyUpdate(update: RoomDirectorySearchEntryUpdate) {
|
||||
when (update) {
|
||||
is RoomDirectorySearchEntryUpdate.Append -> {
|
||||
val roomSummaries = update.values.map(roomDescriptionMapper::map)
|
||||
addAll(roomSummaries)
|
||||
}
|
||||
is RoomDirectorySearchEntryUpdate.PushBack -> {
|
||||
val roomDescription = roomDescriptionMapper.map(update.value)
|
||||
add(roomDescription)
|
||||
}
|
||||
is RoomDirectorySearchEntryUpdate.PushFront -> {
|
||||
val roomDescription = roomDescriptionMapper.map(update.value)
|
||||
add(0, roomDescription)
|
||||
}
|
||||
is RoomDirectorySearchEntryUpdate.Set -> {
|
||||
val roomDescription = roomDescriptionMapper.map(update.value)
|
||||
this[update.index.toInt()] = roomDescription
|
||||
}
|
||||
is RoomDirectorySearchEntryUpdate.Insert -> {
|
||||
val roomDescription = roomDescriptionMapper.map(update.value)
|
||||
add(update.index.toInt(), roomDescription)
|
||||
}
|
||||
is RoomDirectorySearchEntryUpdate.Remove -> {
|
||||
removeAt(update.index.toInt())
|
||||
}
|
||||
is RoomDirectorySearchEntryUpdate.Reset -> {
|
||||
clear()
|
||||
addAll(update.values.map(roomDescriptionMapper::map))
|
||||
}
|
||||
RoomDirectorySearchEntryUpdate.PopBack -> {
|
||||
removeLastOrNull()
|
||||
}
|
||||
RoomDirectorySearchEntryUpdate.PopFront -> {
|
||||
removeFirstOrNull()
|
||||
}
|
||||
RoomDirectorySearchEntryUpdate.Clear -> {
|
||||
clear()
|
||||
}
|
||||
is RoomDirectorySearchEntryUpdate.Truncate -> {
|
||||
subList(update.length.toInt(), size).clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateRoomDescriptions(block: suspend MutableList<RoomDescription>.() -> Unit) = withContext(coroutineContext) {
|
||||
mutex.withLock {
|
||||
val current = roomDescriptions.replayCache.lastOrNull()
|
||||
val mutableRoomSummaries = current.orEmpty().toMutableList()
|
||||
block(mutableRoomSummaries)
|
||||
roomDescriptions.emit(mutableRoomSummaries)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.roomdirectory
|
||||
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.rustcomponents.sdk.RoomDirectorySearch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class RustRoomDirectoryList(
|
||||
private val inner: RoomDirectorySearch,
|
||||
coroutineScope: CoroutineScope,
|
||||
private val coroutineContext: CoroutineContext,
|
||||
) : RoomDirectoryList {
|
||||
private val hasMoreToLoad = MutableStateFlow(true)
|
||||
private val items = MutableSharedFlow<List<RoomDescription>>(replay = 1)
|
||||
private val processor = RoomDirectorySearchProcessor(items, coroutineContext, RoomDescriptionMapper())
|
||||
|
||||
init {
|
||||
launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
private fun launchIn(coroutineScope: CoroutineScope) {
|
||||
inner
|
||||
.resultsFlow()
|
||||
.onEach { updates ->
|
||||
processor.postUpdates(updates)
|
||||
}
|
||||
.flowOn(coroutineContext)
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
override suspend fun filter(filter: String?, batchSize: Int): Result<Unit> {
|
||||
return execute {
|
||||
inner.search(filter = filter, batchSize = batchSize.toUInt())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadMore(): Result<Unit> {
|
||||
return execute {
|
||||
inner.nextPage()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun execute(action: suspend () -> Unit): Result<Unit> {
|
||||
return try {
|
||||
// We always assume there is more to load until we know there isn't.
|
||||
// As accessing hasMoreToLoad is otherwise blocked by the current action.
|
||||
hasMoreToLoad.value = true
|
||||
action()
|
||||
Result.success(Unit)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
} finally {
|
||||
hasMoreToLoad.value = hasMoreToLoad()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun hasMoreToLoad(): Boolean {
|
||||
return !inner.isAtLastPage()
|
||||
}
|
||||
|
||||
override val state: Flow<RoomDirectoryList.State> =
|
||||
combine(hasMoreToLoad, items) { hasMoreToLoad, items ->
|
||||
RoomDirectoryList.State(
|
||||
hasMoreToLoad = hasMoreToLoad,
|
||||
items = items
|
||||
)
|
||||
}
|
||||
.flowOn(coroutineContext)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.roomdirectory
|
||||
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
|
||||
class RustRoomDirectoryService(
|
||||
private val client: Client,
|
||||
private val sessionDispatcher: CoroutineDispatcher,
|
||||
) : RoomDirectoryService {
|
||||
override fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList {
|
||||
return RustRoomDirectoryList(client.roomDirectorySearch(), scope, sessionDispatcher)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String?> = Result.success(null),
|
||||
) : MatrixClient {
|
||||
var setDisplayNameCalled: Boolean = false
|
||||
|
|
@ -91,6 +94,9 @@ class FakeMatrixClient(
|
|||
private var setDisplayNameResult: Result<Unit> = Result.success(Unit)
|
||||
private var uploadAvatarResult: Result<Unit> = Result.success(Unit)
|
||||
private var removeAvatarResult: Result<Unit> = Result.success(Unit)
|
||||
var joinRoomLambda: suspend (RoomId) -> Result<RoomId> = {
|
||||
Result.success(it)
|
||||
}
|
||||
|
||||
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||
return getRoomResults[roomId]
|
||||
|
|
@ -126,6 +132,8 @@ class FakeMatrixClient(
|
|||
|
||||
override fun syncService() = syncService
|
||||
|
||||
override fun roomDirectoryService() = roomDirectoryService
|
||||
|
||||
override suspend fun getCacheSize(): Long {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -176,6 +184,8 @@ class FakeMatrixClient(
|
|||
return removeAvatarResult
|
||||
}
|
||||
|
||||
override suspend fun joinRoom(roomId: RoomId): Result<RoomId> = joinRoomLambda(roomId)
|
||||
|
||||
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService
|
||||
|
||||
override fun pushersService(): PushersService = pushersService
|
||||
|
|
|
|||
|
|
@ -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<RoomDirectoryList.State> = emptyFlow(),
|
||||
val filterLambda: (String?, Int) -> Result<Unit> = { _, _ -> Result.success(Unit) },
|
||||
val loadMoreLambda: () -> Result<Unit> = { Result.success(Unit) }
|
||||
) : RoomDirectoryList {
|
||||
override suspend fun filter(filter: String?, batchSize: Int) = filterLambda(filter, batchSize)
|
||||
|
||||
override suspend fun loadMore(): Result<Unit> = loadMoreLambda()
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class FakeRoomDirectoryService(
|
||||
private val createRoomDirectoryListFactory: (CoroutineScope) -> RoomDirectoryList = { throw AssertionError("Configure a proper factory.") }
|
||||
) : RoomDirectoryService {
|
||||
override fun createRoomDirectoryList(scope: CoroutineScope) = createRoomDirectoryListFactory(scope)
|
||||
}
|
||||
|
|
@ -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,
|
||||
numberOfMembers = joinedMembers
|
||||
)
|
||||
|
|
@ -100,4 +100,9 @@ object TestTags {
|
|||
* Timeline item.
|
||||
*/
|
||||
val timelineItemSenderInfo = TestTag("timeline_item-sender_info")
|
||||
|
||||
/**
|
||||
* Search field.
|
||||
*/
|
||||
val searchTextField = TestTag("search_text_field")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -258,8 +258,6 @@
|
|||
<string name="screen_media_picker_error_failed_selection">"Не ўдалося выбраць носьбіт, паўтарыце спробу."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз."</string>
|
||||
<string name="screen_room_directory_search_loading_error">"Памылка загрузкі"</string>
|
||||
<string name="screen_room_directory_search_title">"Каталог пакояў"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Не ўдалося атрымаць інфармацыю пра карыстальніка"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Заблакіраваць"</string>
|
||||
|
|
|
|||
|
|
@ -258,8 +258,6 @@
|
|||
<string name="screen_media_picker_error_failed_selection">"Výběr média se nezdařil, zkuste to prosím znovu."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
<string name="screen_room_directory_search_loading_error">"Načítání se nezdařilo"</string>
|
||||
<string name="screen_room_directory_search_title">"Adresář místností"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Nepodařilo se načíst údaje o uživateli"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Zablokovat"</string>
|
||||
|
|
|
|||
|
|
@ -254,8 +254,6 @@
|
|||
<string name="screen_media_picker_error_failed_selection">"Medienauswahl fehlgeschlagen, bitte versuche es erneut."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Das Hochladen der Medien ist fehlgeschlagen. Bitte versuche es erneut."</string>
|
||||
<string name="screen_room_directory_search_loading_error">"Fehler beim Laden"</string>
|
||||
<string name="screen_room_directory_search_title">"Raumverzeichnis"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Benutzerdetails konnten nicht abgerufen werden"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Blockieren"</string>
|
||||
|
|
|
|||
|
|
@ -254,8 +254,6 @@
|
|||
<string name="screen_media_picker_error_failed_selection">"Échec de la sélection du média, veuillez réessayer."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Échec du traitement des médias à télécharger, veuillez réessayer."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Échec du téléchargement du média, veuillez réessayer."</string>
|
||||
<string name="screen_room_directory_search_loading_error">"Échec du chargement"</string>
|
||||
<string name="screen_room_directory_search_title">"Annuaire des salons"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Échec du traitement des médias à télécharger, veuillez réessayer."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Impossible de récupérer les détails de l’utilisateur"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Bloquer"</string>
|
||||
|
|
|
|||
|
|
@ -256,8 +256,6 @@
|
|||
<string name="screen_media_picker_error_failed_selection">"Не удалось выбрать носитель, попробуйте еще раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Не удалось загрузить медиафайлы, попробуйте еще раз."</string>
|
||||
<string name="screen_room_directory_search_loading_error">"Сбой загрузки"</string>
|
||||
<string name="screen_room_directory_search_title">"Каталог комнат"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Не удалось получить данные о пользователе"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Заблокировать"</string>
|
||||
|
|
|
|||
|
|
@ -257,8 +257,6 @@
|
|||
<string name="screen_media_picker_error_failed_selection">"Nepodarilo sa vybrať médium, skúste to prosím znova."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Nepodarilo sa nahrať médiá, skúste to prosím znova."</string>
|
||||
<string name="screen_room_directory_search_loading_error">"Načítanie zlyhalo"</string>
|
||||
<string name="screen_room_directory_search_title">"Adresár miestností"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Nepodarilo sa získať údaje o používateľovi"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Zablokovať"</string>
|
||||
|
|
|
|||
|
|
@ -256,8 +256,6 @@
|
|||
<string name="screen_media_picker_error_failed_selection">"Не вдалося вибрати медіафайл, спробуйте ще раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Не вдалося обробити медіафайл для завантаження, спробуйте ще раз."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Не вдалося завантажити медіафайл, спробуйте ще раз."</string>
|
||||
<string name="screen_room_directory_search_loading_error">"Не вдалося завантажити"</string>
|
||||
<string name="screen_room_directory_search_title">"Каталог кімнат"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не вдалося обробити медіафайл для завантаження, спробуйте ще раз."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Не вдалося отримати дані користувача"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Заблокувати"</string>
|
||||
|
|
|
|||
|
|
@ -254,8 +254,6 @@
|
|||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
|
||||
<string name="screen_room_directory_search_loading_error">"Failed loading"</string>
|
||||
<string name="screen_room_directory_search_title">"Room directory"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
||||
<string name="screen_room_member_details_block_alert_action">"Block"</string>
|
||||
|
|
@ -274,4 +272,55 @@
|
|||
<string name="settings_version_number">"Version: %1$s (%2$s)"</string>
|
||||
<string name="test_language_identifier">"en"</string>
|
||||
<string name="test_untranslated_default_language_identifier">"en"</string>
|
||||
<string name="troubleshoot_notifications_entry_point_section">"Troubleshoot"</string>
|
||||
<string name="troubleshoot_notifications_entry_point_title">"Troubleshoot notifications"</string>
|
||||
<string name="troubleshoot_notifications_screen_action">"Run tests"</string>
|
||||
<string name="troubleshoot_notifications_screen_action_again">"Run tests again"</string>
|
||||
<string name="troubleshoot_notifications_screen_failure">"Some tests failed. Please check the details."</string>
|
||||
<string name="troubleshoot_notifications_screen_notice">"Run the tests to detect any issue in your configuration that may make notifications not behave as expected."</string>
|
||||
<string name="troubleshoot_notifications_screen_quick_fix_action">"Attempt to fix"</string>
|
||||
<string name="troubleshoot_notifications_screen_success">"All tests passed successfully."</string>
|
||||
<string name="troubleshoot_notifications_screen_title">"Troubleshoot notifications"</string>
|
||||
<string name="troubleshoot_notifications_screen_waiting">"Some tests require your attention. Please check the details."</string>
|
||||
<string name="troubleshoot_notifications_test_check_permission_description">"Check that the application can show notifications."</string>
|
||||
<string name="troubleshoot_notifications_test_check_permission_title">"Check permissions"</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_description">"Get the name of the current provider."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_failure">"No push providers selected."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_success">"Current push provider: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_current_push_provider_title">"Current push provider"</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_description">"Ensure that the application has at least one push provider."</string>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_failure">"No push providers found."</string>
|
||||
<plurals name="troubleshoot_notifications_test_detect_push_provider_success">
|
||||
<item quantity="one">"Found %1$d push provider: %2$s"</item>
|
||||
<item quantity="other">"Found %1$d push providers: %2$s"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_detect_push_provider_title">"Detect push providers"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_description">"Check that the application can display notification."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_failure">"The notification has not been clicked."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_permission_failure">"Cannot display the notification."</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_success">"The notification has been clicked!"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_title">"Display notification"</string>
|
||||
<string name="troubleshoot_notifications_test_display_notification_waiting">"Please click on the notification to continue the test."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_description">"Ensure that Firebase is available."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_failure">"Firebase is not available."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_success">"Firebase is available."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_availability_title">"Check Firebase"</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_description">"Ensure that Firebase token is available."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_failure">"Firebase token is not known."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_success">"Firebase token: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_firebase_token_title">"Check Firebase token"</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_description">"Ensure that the application is receiving push."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_1">"Error: pusher has rejected the request."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_2">"Error: %1$s."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_3">"Error, cannot test push."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_failure_4">"Error, timeout waiting for push."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_success">"Push loop back took %1$d ms."</string>
|
||||
<string name="troubleshoot_notifications_test_push_loop_back_title">"Test Push loop back"</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Ensure that UnifiedPush distributors are available."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"No push distributors found."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d distributor found: %2$s."</item>
|
||||
<item quantity="other">"%1$d distributors found: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Check UnifiedPush"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <State> Presenter<State>.test(
|
||||
timeout: Duration? = null,
|
||||
name: String? = null,
|
||||
validate: suspend TurbineTestContext<State>.() -> Unit,
|
||||
) {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
present()
|
||||
}.test(timeout, name, validate)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1b6110eb2eb66c404f787472dd18608a27a3c8ce19c794ed4b3f67e186720978
|
||||
size 13703
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c2b6cc7995880af654439c4f45d6e3ff35ff9c92609ad136fcdfbb2050edea44
|
||||
size 30484
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1f7885c0ee25d12dc5bb25940b35e7153f95ffc35cbe6fac21839390d52faeac
|
||||
size 32326
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:776d8f8a15bac930896a8baaf46f69fddf24fdf5c51c1ab6f1bbc48ef89259d6
|
||||
size 31607
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b349aa0ce78db6396094adda401f52133c32c16e695a051ba115be0143c31de9
|
||||
size 34972
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9eafd587065c22d19814285b63e7690f5ff4bd1ed0765bd9802e61d6d8ed0ba7
|
||||
size 12915
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7a7b5c11dbc45a804d34a0105c49c7bd15ab1f5d6441f0e95dba84fd8a9eada6
|
||||
size 29080
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6af6d92766e7249ee95582a32ac17ddd5c3bcc29c9b78d9ba9ca6bcb27b69c8c
|
||||
size 30828
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b434b666666da81963d8ac89e9669f059443cd529103735944e6a15ba5ba46b5
|
||||
size 28646
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f0190f4e7d06f5b46147531d7224e24662e0a7ef6289d516cf082d59ab63627f
|
||||
size 31342
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1625ac34428f660c235d1d64f5de867baa6c0ca296f0a93d81588d633d6a74bf
|
||||
size 30082
|
||||
oid sha256:615e00dfa47d40fd459d268d6e68c61a501be7e4a24bfe7d18f3a647da8c5f08
|
||||
size 10595
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1625ac34428f660c235d1d64f5de867baa6c0ca296f0a93d81588d633d6a74bf
|
||||
size 30082
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:264d9373767b6b59d0cea4bfa4d148a453058be70e7805581fe96d7448ef5232
|
||||
size 29978
|
||||
oid sha256:421e47de6c2bc7fd0a5de84b6db4bea79c7312442244ec11506164150762943b
|
||||
size 9787
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:264d9373767b6b59d0cea4bfa4d148a453058be70e7805581fe96d7448ef5232
|
||||
size 29978
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c7c46e25d0b339f237f5c5d98d850664638b2084b3fe4a75e5d7b50d3b678b8b
|
||||
size 18999
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:150055f181351f24e22ba29492904b2296edc7c826aaa81952216734ec4431f9
|
||||
size 18098
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a623b5b5bd5700f38b9eb790a37a5b029e1a74b8ae9811f9732dec808cacc770
|
||||
size 21231
|
||||
|
|
@ -197,6 +197,12 @@
|
|||
"screen_app_lock_.*",
|
||||
"screen_signout_in_progress_dialog_content"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:roomdirectory:impl",
|
||||
"includeRegex" : [
|
||||
"screen_room_directory_.*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue