Merge pull request #2695 from element-hq/feature/fga/room_navigation

Room navigation
This commit is contained in:
ganfra 2024-04-15 12:55:29 +02:00 committed by GitHub
commit 1dcdc713a4
133 changed files with 2824 additions and 934 deletions

View file

@ -25,6 +25,7 @@ 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.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
@ -35,9 +36,15 @@ class RoomDirectoryNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>,
private val presenter: RoomDirectoryPresenter,
) : Node(buildContext, plugins = plugins) {
private fun onResultClicked(roomDescription: RoomDescription) {
plugins<RoomDirectoryEntryPoint.Callback>().forEach {
it.onResultClicked(roomDescription)
}
}
private fun onRoomJoined(roomId: RoomId) {
plugins<RoomDirectoryEntryPoint.Callback>().forEach {
it.onOpenRoom(roomId)
it.onRoomJoined(roomId)
}
}
@ -47,6 +54,7 @@ class RoomDirectoryNode @AssistedInject constructor(
RoomDirectoryView(
state = state,
onRoomJoined = ::onRoomJoined,
onResultClicked = ::onResultClicked,
onBackPressed = ::navigateUp,
modifier = modifier
)

View file

@ -19,8 +19,6 @@ 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
@ -70,26 +68,20 @@ fun aRoomDescriptionList(): ImmutableList<RoomDescription> {
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,
topic = "Element X is a secure, private and decentralized messenger.",
alias = "#element-x-android:matrix.org",
avatarUrl = null,
joinRule = RoomDescription.JoinRule.PUBLIC,
numberOfMembers = 2765,
),
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,
topic = "Element X is a secure, private and decentralized messenger.",
alias = "#element-x-ios:matrix.org",
avatarUrl = null,
joinRule = RoomDescription.JoinRule.UNKNOWN,
numberOfMembers = 356,
)
)
}

View file

@ -51,6 +51,7 @@ 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.avatar.AvatarSize
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
@ -70,12 +71,13 @@ import kotlinx.collections.immutable.ImmutableList
@Composable
fun RoomDirectoryView(
state: RoomDirectoryState,
onResultClicked: (RoomDescription) -> Unit,
onRoomJoined: (RoomId) -> Unit,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
fun joinRoom(roomId: RoomId) {
state.eventSink(RoomDirectoryEvents.JoinRoom(roomId))
fun joinRoom(roomDescription: RoomDescription) {
state.eventSink(RoomDirectoryEvents.JoinRoom(roomDescription.roomId))
}
Scaffold(
@ -86,10 +88,11 @@ fun RoomDirectoryView(
content = { padding ->
RoomDirectoryContent(
state = state,
onResultClicked = ::joinRoom,
onResultClicked = onResultClicked,
onJoinClicked = ::joinRoom,
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.padding(padding)
.consumeWindowInsets(padding)
)
}
)
@ -128,7 +131,8 @@ private fun RoomDirectoryTopBar(
@Composable
private fun RoomDirectoryContent(
state: RoomDirectoryState,
onResultClicked: (RoomId) -> Unit,
onResultClicked: (RoomDescription) -> Unit,
onJoinClicked: (RoomDescription) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
@ -143,6 +147,7 @@ private fun RoomDirectoryContent(
displayLoadMoreIndicator = state.displayLoadMoreIndicator,
displayEmptyState = state.displayEmptyState,
onResultClicked = onResultClicked,
onJoinClicked = onJoinClicked,
onReachedLoadMore = { state.eventSink(RoomDirectoryEvents.LoadMore) },
)
}
@ -153,7 +158,8 @@ private fun RoomDirectoryRoomList(
roomDescriptions: ImmutableList<RoomDescription>,
displayLoadMoreIndicator: Boolean,
displayEmptyState: Boolean,
onResultClicked: (RoomId) -> Unit,
onResultClicked: (RoomDescription) -> Unit,
onJoinClicked: (RoomDescription) -> Unit,
onReachedLoadMore: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -161,7 +167,12 @@ private fun RoomDirectoryRoomList(
items(roomDescriptions) { roomDescription ->
RoomDirectoryRoomRow(
roomDescription = roomDescription,
onClick = onResultClicked,
onClick = {
onResultClicked(roomDescription)
},
onJoinClick = {
onJoinClicked(roomDescription)
},
)
}
if (displayEmptyState) {
@ -188,10 +199,10 @@ private fun RoomDirectoryRoomList(
@Composable
private fun LoadMoreIndicator(modifier: Modifier = Modifier) {
Box(
modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(24.dp),
modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(24.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
@ -256,53 +267,54 @@ private fun SearchTextField(
@Composable
private fun RoomDirectoryRoomRow(
roomDescription: RoomDescription,
onClick: (RoomId) -> Unit,
onClick: () -> Unit,
onJoinClick: () -> 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),
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(
top = 12.dp,
bottom = 12.dp,
start = 16.dp,
)
.height(IntrinsicSize.Min),
) {
Avatar(
avatarData = roomDescription.avatarData,
avatarData = roomDescription.avatarData(AvatarSize.RoomDirectoryItem),
modifier = Modifier.align(Alignment.CenterVertically)
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
.weight(1f)
.padding(start = 16.dp)
) {
Text(
text = roomDescription.name,
text = roomDescription.computedName,
maxLines = 1,
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textPrimary,
overflow = TextOverflow.Ellipsis,
)
Text(
text = roomDescription.description,
text = roomDescription.computedDescription,
maxLines = 1,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
overflow = TextOverflow.Ellipsis,
)
}
if (roomDescription.canBeJoined) {
if (roomDescription.canJoinOrKnock) {
Text(
text = stringResource(id = CommonStrings.action_join),
color = ElementTheme.colors.textSuccessPrimary,
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 4.dp, end = 12.dp)
.align(Alignment.CenterVertically)
.clickable(onClick = onJoinClick)
.padding(start = 4.dp, end = 12.dp)
.testTag(TestTags.callToAction.value)
)
} else {
Spacer(modifier = Modifier.width(24.dp))
@ -315,6 +327,7 @@ private fun RoomDirectoryRoomRow(
internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview {
RoomDirectoryView(
state = state,
onResultClicked = {},
onRoomJoined = {},
onBackPressed = {},
)

View file

@ -17,37 +17,20 @@
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,
name = name,
alias = alias,
topic = topic,
avatarUrl = avatarUrl,
numberOfMembers = numberOfMembers,
joinRule = when (joinRule) {
MatrixRoomDescription.JoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC
MatrixRoomDescription.JoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK
MatrixRoomDescription.JoinRule.UNKNOWN -> RoomDescription.JoinRule.UNKNOWN
}
)
}

View file

@ -19,11 +19,14 @@ 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.onAllNodesWithTag
import androidx.compose.ui.test.onFirst
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.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.testtags.TestTags
@ -55,7 +58,24 @@ class RoomDirectoryViewTest {
}
@Test
fun `clicking on room item emits the expected Event`() {
fun `clicking on room item then onResultClicked lambda is called once`() {
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
val state = aRoomDirectoryState(
roomDescriptions = aRoomDescriptionList(),
eventSink = eventsRecorder,
)
val clickedRoom = state.roomDescriptions.first()
ensureCalledOnceWithParam(clickedRoom) { callback ->
rule.setRoomDirectoryView(
state = state,
onResultClicked = callback,
)
rule.onNodeWithText(clickedRoom.computedName).performClick()
}
}
@Test
fun `clicking on room item join cta emits the expected Event`() {
val eventsRecorder = EventsRecorder<RoomDirectoryEvents>()
val state = aRoomDirectoryState(
roomDescriptions = aRoomDescriptionList(),
@ -63,7 +83,7 @@ class RoomDirectoryViewTest {
)
rule.setRoomDirectoryView(state = state)
val clickedRoom = state.roomDescriptions.first()
rule.onNodeWithText(clickedRoom.name).performClick()
rule.onAllNodesWithTag(TestTags.callToAction.value).onFirst().performClick()
eventsRecorder.assertSingle(RoomDirectoryEvents.JoinRoom(clickedRoom.roomId))
}
@ -100,11 +120,13 @@ class RoomDirectoryViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDirectoryView(
state: RoomDirectoryState,
onBackPressed: () -> Unit = EnsureNeverCalled(),
onResultClicked: (RoomDescription) -> Unit = EnsureNeverCalledWithParam(),
onRoomJoined: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
) {
setContent {
RoomDirectoryView(
state = state,
onResultClicked = onResultClicked,
onRoomJoined = onRoomJoined,
onBackPressed = onBackPressed,
)