feature(space): make sure to handle topic properly

This commit is contained in:
ganfra 2025-10-08 21:59:12 +02:00
parent eb51b5cc3f
commit 6f4af1dda4
7 changed files with 102 additions and 3 deletions

View file

@ -15,4 +15,7 @@ sealed interface SpaceEvents {
data object ClearFailures : SpaceEvents data object ClearFailures : SpaceEvents
data class AcceptInvite(val spaceRoom: SpaceRoom) : SpaceEvents data class AcceptInvite(val spaceRoom: SpaceRoom) : SpaceEvents
data class DeclineInvite(val spaceRoom: SpaceRoom) : SpaceEvents data class DeclineInvite(val spaceRoom: SpaceRoom) : SpaceEvents
data class ShowTopicViewer(val topic: String) : SpaceEvents
data object HideTopicViewer : SpaceEvents
} }

View file

@ -80,6 +80,8 @@ class SpacePresenter(
val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState() val currentSpace by spaceRoomList.currentSpaceFlow.collectAsState()
val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) } val (joinActions, setJoinActions) = remember { mutableStateOf(emptyMap<RoomId, AsyncAction<Unit>>()) }
var topicViewerState: TopicViewerState by remember { mutableStateOf(TopicViewerState.Hidden) }
LaunchedEffect(children) { LaunchedEffect(children) {
// Remove joined children from the join actions // Remove joined children from the join actions
val joinedChildren = children val joinedChildren = children
@ -112,6 +114,8 @@ class SpacePresenter(
AcceptDeclineInviteEvents.DeclineInvite(invite = event.spaceRoom.toInviteData(), shouldConfirm = true, blockUser = false) 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( return SpaceState(
@ -122,6 +126,7 @@ class SpacePresenter(
hasMoreToLoad = hasMoreToLoad, hasMoreToLoad = hasMoreToLoad,
joinActions = joinActions.toPersistentMap(), joinActions = joinActions.toPersistentMap(),
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
topicViewerState = topicViewerState,
eventSink = ::handleEvents, eventSink = ::handleEvents,
) )
} }

View file

@ -7,6 +7,7 @@
package io.element.android.features.space.impl.root 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.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -23,6 +24,7 @@ data class SpaceState(
val hasMoreToLoad: Boolean, val hasMoreToLoad: Boolean,
val joinActions: ImmutableMap<RoomId, AsyncAction<Unit>>, val joinActions: ImmutableMap<RoomId, AsyncAction<Unit>>,
val acceptDeclineInviteState: AcceptDeclineInviteState, val acceptDeclineInviteState: AcceptDeclineInviteState,
val topicViewerState: TopicViewerState,
val eventSink: (SpaceEvents) -> Unit val eventSink: (SpaceEvents) -> Unit
) { ) {
fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading
@ -30,3 +32,9 @@ data class SpaceState(
it is AsyncAction.Failure it is AsyncAction.Failure
} }
} }
@Immutable
sealed interface TopicViewerState {
data object Hidden : TopicViewerState
data class Shown(val topic: String) : TopicViewerState
}

View file

@ -51,7 +51,17 @@ open class SpaceStateProvider : PreviewParameterProvider<SpaceState> {
hasMoreToLoad = false, hasMoreToLoad = false,
children = aListOfSpaceRooms(), children = aListOfSpaceRooms(),
joiningRooms = setOf(RoomId("!spaceId0:example.com")), 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 // Add other states here
) )
} }
@ -70,6 +80,7 @@ fun aSpaceState(
hideInvitesAvatar: Boolean = false, hideInvitesAvatar: Boolean = false,
hasMoreToLoad: Boolean = false, hasMoreToLoad: Boolean = false,
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
topicViewerState: TopicViewerState = TopicViewerState.Hidden,
eventSink: (SpaceEvents) -> Unit = { }, eventSink: (SpaceEvents) -> Unit = { },
) = SpaceState( ) = SpaceState(
currentSpace = parentSpace, currentSpace = parentSpace,
@ -79,6 +90,7 @@ fun aSpaceState(
hasMoreToLoad = hasMoreToLoad, hasMoreToLoad = hasMoreToLoad,
joinActions = joinActions.toImmutableMap(), joinActions = joinActions.toImmutableMap(),
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
topicViewerState = topicViewerState,
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -7,13 +7,18 @@
package io.element.android.features.space.impl.root 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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule 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.AsyncIndicator
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState 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.DropdownMenuItem
import io.element.android.libraries.designsystem.theme.components.Icon 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.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.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar 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.collections.immutable.toImmutableList
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SpaceView( fun SpaceView(
state: SpaceState, state: SpaceState,
@ -87,7 +95,10 @@ fun SpaceView(
) { ) {
SpaceViewContent( SpaceViewContent(
state = state, state = state,
onRoomClick = onRoomClick onRoomClick = onRoomClick,
onTopicClick = { topic ->
state.eventSink(SpaceEvents.ShowTopicViewer(topic))
}
) )
JoinRoomFailureEffect( JoinRoomFailureEffect(
hasAnyFailure = state.hasAnyFailure, 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 @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 @Composable
private fun SpaceViewContent( private fun SpaceViewContent(
state: SpaceState, state: SpaceState,
onRoomClick: (spaceRoom: SpaceRoom) -> Unit, onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn(modifier.fillMaxSize()) { LazyColumn(modifier.fillMaxSize()) {
@ -134,9 +187,11 @@ private fun SpaceViewContent(
avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader), avatarData = currentSpace.getAvatarData(AvatarSize.SpaceHeader),
name = currentSpace.displayName, name = currentSpace.displayName,
topic = currentSpace.topic, topic = currentSpace.topic,
topicMaxLines = 2,
visibility = currentSpace.visibility, visibility = currentSpace.visibility,
heroes = currentSpace.heroes.toImmutableList(), heroes = currentSpace.heroes.toImmutableList(),
numberOfMembers = currentSpace.numJoinedMembers, numberOfMembers = currentSpace.numJoinedMembers,
onTopicClick = onTopicClick
) )
} }
} }

View file

@ -20,6 +20,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@ -51,6 +52,7 @@ fun ClickableLinkText(
onClick: () -> Unit = {}, onClick: () -> Unit = {},
onLongClick: () -> Unit = {}, onLongClick: () -> Unit = {},
style: TextStyle = LocalTextStyle.current, style: TextStyle = LocalTextStyle.current,
color: Color = Color.Unspecified,
inlineContent: ImmutableMap<String, InlineTextContent> = persistentMapOf(), inlineContent: ImmutableMap<String, InlineTextContent> = persistentMapOf(),
) { ) {
ClickableLinkText( ClickableLinkText(
@ -62,6 +64,7 @@ fun ClickableLinkText(
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
style = style, style = style,
color = color,
inlineContent = inlineContent, inlineContent = inlineContent,
) )
} }
@ -76,6 +79,7 @@ fun ClickableLinkText(
onClick: () -> Unit = {}, onClick: () -> Unit = {},
onLongClick: () -> Unit = {}, onLongClick: () -> Unit = {},
style: TextStyle = LocalTextStyle.current, style: TextStyle = LocalTextStyle.current,
color: Color = Color.Unspecified,
inlineContent: ImmutableMap<String, InlineTextContent> = persistentMapOf(), inlineContent: ImmutableMap<String, InlineTextContent> = persistentMapOf(),
) { ) {
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
@ -126,6 +130,7 @@ fun ClickableLinkText(
text = annotatedString, text = annotatedString,
modifier = modifier.then(pressIndicator), modifier = modifier.then(pressIndicator),
style = style, style = style,
color = color,
onTextLayout = { onTextLayout = {
layoutResult.value = it layoutResult.value = it
}, },

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.ui.components package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -43,6 +44,7 @@ fun SpaceHeaderView(
numberOfMembers: Int, numberOfMembers: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
topicMaxLines: Int = Int.MAX_VALUE, topicMaxLines: Int = Int.MAX_VALUE,
onTopicClick: ((String) -> Unit)? = null,
) { ) {
RoomPreviewOrganism( RoomPreviewOrganism(
modifier = modifier.padding(24.dp), modifier = modifier.padding(24.dp),
@ -68,7 +70,16 @@ fun SpaceHeaderView(
description = if (topic.isNullOrBlank()) { description = if (topic.isNullOrBlank()) {
null null
} else { } else {
{ RoomPreviewDescriptionAtom(description = topic, maxLines = topicMaxLines) } {
RoomPreviewDescriptionAtom(
description = topic,
maxLines = topicMaxLines,
modifier = Modifier.clickable(
enabled = onTopicClick != null,
onClick = { onTopicClick?.invoke(topic) }
)
)
}
}, },
memberCount = { memberCount = {
SpaceMembersView( SpaceMembersView(