From 9d05eb8e6fe7fcc55fdcadc7e992ccdba562c215 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Oct 2025 21:59:12 +0200 Subject: [PATCH] feature(space): make sure to handle topic properly --- .../features/space/impl/root/SpaceEvents.kt | 3 + .../space/impl/root/SpacePresenter.kt | 5 ++ .../features/space/impl/root/SpaceState.kt | 8 +++ .../space/impl/root/SpaceStateProvider.kt | 14 ++++- .../features/space/impl/root/SpaceView.kt | 57 ++++++++++++++++++- .../components/ClickableLinkText.kt | 5 ++ .../matrix/ui/components/SpaceHeaderView.kt | 13 ++++- 7 files changed, 102 insertions(+), 3 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt index ab94ef719b..8c8eb8dbb7 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt @@ -15,4 +15,7 @@ sealed interface SpaceEvents { data object ClearFailures : SpaceEvents data class AcceptInvite(val spaceRoom: SpaceRoom) : SpaceEvents data class DeclineInvite(val spaceRoom: SpaceRoom) : SpaceEvents + + data class ShowTopicViewer(val topic: String) : SpaceEvents + data object HideTopicViewer : SpaceEvents } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index fdd090066e..faa4ca38d4 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -80,6 +80,8 @@ class SpacePresenter( val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap>()) } + var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) } + LaunchedEffect(children) { // Remove joined children from the join actions val joinedChildren = children @@ -112,6 +114,8 @@ class SpacePresenter( AcceptDeclineInviteEvents.DeclineInvite(invite = event.spaceRoom.toInviteData(), shouldConfirm = true, blockUser = false) ) } + SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden + is SpaceEvents.ShowTopicViewer -> topicViewerState = TopicViewerState.Shown(event.topic) } } return SpaceState( @@ -122,6 +126,7 @@ class SpacePresenter( hasMoreToLoad = hasMoreToLoad, joinActions = joinActions.toPersistentMap(), acceptDeclineInviteState = acceptDeclineInviteState, + topicViewerState = topicViewerState, eventSink = ::handleEvents, ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index ed6bc3dcf7..2499e4c046 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -7,6 +7,7 @@ package io.element.android.features.space.impl.root +import androidx.compose.runtime.Immutable import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId @@ -23,6 +24,7 @@ data class SpaceState( val hasMoreToLoad: Boolean, val joinActions: ImmutableMap>, val acceptDeclineInviteState: AcceptDeclineInviteState, + val topicViewerState: TopicViewerState, val eventSink: (SpaceEvents) -> Unit ) { fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading @@ -30,3 +32,9 @@ data class SpaceState( it is AsyncAction.Failure } } + +@Immutable +sealed interface TopicViewerState { + data object Hidden : TopicViewerState + data class Shown(val topic: String) : TopicViewerState +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt index e58e7e82f4..a9ca00776c 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt @@ -51,7 +51,17 @@ open class SpaceStateProvider : PreviewParameterProvider { hasMoreToLoad = false, children = aListOfSpaceRooms(), joiningRooms = setOf(RoomId("!spaceId0:example.com")), - ) + ), + aSpaceState( + hasMoreToLoad = false, + topicViewerState = TopicViewerState.Shown( + topic = "Description of the space goes right here. Lorem ipsum dolor sit amet consectetur. " + + "Leo viverra morbi habitant in. Sem amet enim habitant nibh augue mauris. " + + "Interdum mauris ultrices tincidunt proin morbi erat aenean risus nibh. " + + "Diam amet sit fermentum vulputate faucibus." + ), + children = aListOfSpaceRooms(), + ), // Add other states here ) } @@ -70,6 +80,7 @@ fun aSpaceState( hideInvitesAvatar: Boolean = false, hasMoreToLoad: Boolean = false, acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), + topicViewerState: TopicViewerState = TopicViewerState.Hidden, eventSink: (SpaceEvents) -> Unit = { }, ) = SpaceState( currentSpace = parentSpace, @@ -79,6 +90,7 @@ fun aSpaceState( hasMoreToLoad = hasMoreToLoad, joinActions = joinActions.toImmutableMap(), acceptDeclineInviteState = acceptDeclineInviteState, + topicViewerState = topicViewerState, eventSink = eventSink, ) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index ad2a34a848..463d07d907 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -7,13 +7,18 @@ package io.element.android.features.space.impl.root +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -33,6 +38,7 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule +import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.components.async.AsyncIndicator import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState @@ -48,6 +54,7 @@ import io.element.android.libraries.designsystem.theme.components.DropdownMenu import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem 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.ModalBottomSheet 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.TopAppBar @@ -61,6 +68,7 @@ import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SpaceView( state: SpaceState, @@ -87,7 +95,10 @@ fun SpaceView( ) { SpaceViewContent( state = state, - onRoomClick = onRoomClick + onRoomClick = onRoomClick, + onTopicClick = { topic -> + state.eventSink(SpaceEvents.ShowTopicViewer(topic)) + } ) JoinRoomFailureEffect( hasAnyFailure = state.hasAnyFailure, @@ -97,6 +108,14 @@ fun SpaceView( } }, ) + if (state.topicViewerState is TopicViewerState.Shown) { + TopicViewerBottomSheet( + topicViewerState = state.topicViewerState, + onDismiss = { + state.eventSink(SpaceEvents.HideTopicViewer) + } + ) + } } @Composable @@ -120,10 +139,44 @@ private fun JoinRoomFailureEffect( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopicViewerBottomSheet( + topicViewerState: TopicViewerState.Shown, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + ModalBottomSheet( + onDismissRequest = onDismiss, + modifier = modifier, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Text( + stringResource(CommonStrings.common_description), + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textPrimary, + ) + Spacer(Modifier.height(8.dp)) + ClickableLinkText( + text = topicViewerState.topic, + interactionSource = remember { MutableInteractionSource() }, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + ) + } + } +} + @Composable private fun SpaceViewContent( state: SpaceState, onRoomClick: (spaceRoom: SpaceRoom) -> Unit, + onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn(modifier.fillMaxSize()) { @@ -134,9 +187,11 @@ private fun SpaceViewContent( avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader), name = currentSpace.displayName, topic = currentSpace.topic, + topicMaxLines = 2, visibility = currentSpace.visibility, heroes = currentSpace.heroes.toImmutableList(), numberOfMembers = currentSpace.numJoinedMembers, + onTopicClick = onTopicClick ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt index 3638556da3..6c6d6d398e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString @@ -51,6 +52,7 @@ fun ClickableLinkText( onClick: () -> Unit = {}, onLongClick: () -> Unit = {}, style: TextStyle = LocalTextStyle.current, + color: Color = Color.Unspecified, inlineContent: ImmutableMap = persistentMapOf(), ) { ClickableLinkText( @@ -62,6 +64,7 @@ fun ClickableLinkText( onClick = onClick, onLongClick = onLongClick, style = style, + color = color, inlineContent = inlineContent, ) } @@ -76,6 +79,7 @@ fun ClickableLinkText( onClick: () -> Unit = {}, onLongClick: () -> Unit = {}, style: TextStyle = LocalTextStyle.current, + color: Color = Color.Unspecified, inlineContent: ImmutableMap = persistentMapOf(), ) { @Suppress("NAME_SHADOWING") @@ -126,6 +130,7 @@ fun ClickableLinkText( text = annotatedString, modifier = modifier.then(pressIndicator), style = style, + color = color, onTextLayout = { layoutResult.value = it }, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderView.kt index e59049f9f2..e9e151f027 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceHeaderView.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.ui.components +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -43,6 +44,7 @@ fun SpaceHeaderView( numberOfMembers: Int, modifier: Modifier = Modifier, topicMaxLines: Int = Int.MAX_VALUE, + onTopicClick: ((String) -> Unit)? = null, ) { RoomPreviewOrganism( modifier = modifier.padding(24.dp), @@ -68,7 +70,16 @@ fun SpaceHeaderView( description = if (topic.isNullOrBlank()) { null } else { - { RoomPreviewDescriptionAtom(description = topic, maxLines = topicMaxLines) } + { + RoomPreviewDescriptionAtom( + description = topic, + maxLines = topicMaxLines, + modifier = Modifier.clickable( + enabled = onTopicClick != null, + onClick = { onTopicClick?.invoke(topic) } + ) + ) + } }, memberCount = { SpaceMembersView(