Merge pull request #5273 from element-hq/feature/bma/spaceNextStep

Space: add content in home screen
This commit is contained in:
Benoit Marty 2025-09-05 17:55:14 +02:00 committed by GitHub
commit a2dd455f22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
146 changed files with 1298 additions and 250 deletions

View file

@ -18,6 +18,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.architecture.Presenter
@ -36,6 +37,7 @@ class HomePresenter(
private val snackbarDispatcher: SnackbarDispatcher,
private val indicatorService: IndicatorService,
private val roomListPresenter: Presenter<RoomListState>,
private val homeSpacesPresenter: Presenter<HomeSpacesState>,
private val logoutPresenter: Presenter<DirectLogoutState>,
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val featureFlagService: FeatureFlagService,
@ -46,6 +48,7 @@ class HomePresenter(
val isOnline by syncService.isOnline.collectAsState()
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
val roomListState = roomListPresenter.present()
val homeSpacesState = homeSpacesPresenter.present()
val isSpaceFeatureEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
}.collectAsState(initial = false)
@ -78,6 +81,7 @@ class HomePresenter(
hasNetworkConnection = isOnline,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
roomListState = roomListState,
homeSpacesState = homeSpacesState,
snackbarMessage = snackbarMessage,
canReportBug = canReportBug,
directLogoutState = directLogoutState,

View file

@ -9,6 +9,7 @@ package io.element.android.features.home.impl
import androidx.compose.runtime.Immutable
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.user.MatrixUser
@ -20,6 +21,7 @@ data class HomeState(
val hasNetworkConnection: Boolean,
val currentHomeNavigationBarItem: HomeNavigationBarItem,
val roomListState: RoomListState,
val homeSpacesState: HomeSpacesState,
val snackbarMessage: SnackbarMessage?,
val canReportBug: Boolean,
val directLogoutState: DirectLogoutState,

View file

@ -13,6 +13,8 @@ import io.element.android.features.home.impl.roomlist.RoomListStateProvider
import io.element.android.features.home.impl.roomlist.aRoomListState
import io.element.android.features.home.impl.roomlist.aRoomsContentState
import io.element.android.features.home.impl.roomlist.generateRoomListRoomSummaryList
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.features.home.impl.spaces.aHomeSpacesState
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
@ -51,6 +53,7 @@ internal fun aHomeState(
snackbarMessage: SnackbarMessage? = null,
currentHomeNavigationBarItem: HomeNavigationBarItem = HomeNavigationBarItem.Chats,
roomListState: RoomListState = aRoomListState(),
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
canReportBug: Boolean = true,
isSpaceFeatureEnabled: Boolean = false,
directLogoutState: DirectLogoutState = aDirectLogoutState(),
@ -64,6 +67,7 @@ internal fun aHomeState(
directLogoutState = directLogoutState,
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
roomListState = roomListState,
homeSpacesState = homeSpacesState,
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
eventSink = eventSink,
)

View file

@ -25,7 +25,6 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -50,6 +49,7 @@ import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.features.home.impl.roomlist.RoomListState
import io.element.android.features.home.impl.search.RoomListSearchView
import io.element.android.features.home.impl.spaces.HomeSpacesView
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.designsystem.preview.ElementPreview
@ -61,7 +61,6 @@ import io.element.android.libraries.designsystem.theme.components.NavigationBarI
import io.element.android.libraries.designsystem.theme.components.NavigationBarItem
import io.element.android.libraries.designsystem.theme.components.NavigationBarText
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.RoomId
@ -262,19 +261,17 @@ private fun HomeScaffold(
)
}
HomeNavigationBarItem.Spaces -> {
Box(
HomeSpacesView(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.consumeWindowInsets(padding)
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = "Spaces are coming soon!",
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textPrimary,
)
}
.hazeSource(state = hazeState),
state = state.homeSpacesState,
onSpaceClick = { spaceId ->
// TODO
}
)
}
}
},

View file

@ -45,15 +45,13 @@ import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.roomlist.RoomListEvents
import io.element.android.libraries.core.extensions.orEmpty
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
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.ButtonSize
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.roomListRoomMessage
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
@ -101,7 +99,7 @@ internal fun RoomSummaryRow(
)
}
Spacer(modifier = Modifier.height(12.dp))
InviteButtonsRow(
InviteButtonsRowMolecule(
onAcceptClick = {
eventSink(RoomListEvents.AcceptInvite(room))
},
@ -346,31 +344,6 @@ private fun InviteNameAndIndicatorRow(
}
}
@Composable
private fun InviteButtonsRow(
onAcceptClick: () -> Unit,
onDeclineClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier,
horizontalArrangement = spacedBy(12.dp)
) {
OutlinedButton(
text = stringResource(CommonStrings.action_decline),
onClick = onDeclineClick,
size = ButtonSize.MediumLowPadding,
modifier = Modifier.weight(1f),
)
Button(
text = stringResource(CommonStrings.action_accept),
onClick = onAcceptClick,
size = ButtonSize.MediumLowPadding,
modifier = Modifier.weight(1f),
)
}
}
@Composable
private fun OnGoingCallIcon(
color: Color,

View file

@ -0,0 +1,23 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.di
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.home.impl.spaces.HomeSpacesPresenter
import io.element.android.features.home.impl.spaces.HomeSpacesState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope
@BindingContainer
@ContributesTo(SessionScope::class)
interface HomeSpacesModule {
@Binds
fun bindHomeSpacesPresenter(presenter: HomeSpacesPresenter): Presenter<HomeSpacesState>
}

View file

@ -0,0 +1,197 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
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.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
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.avatar.AvatarType
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.unreadIndicator
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonPlurals
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun HomeSpaceItemView(
spaceRoom: SpaceRoom,
showUnreadIndicator: Boolean,
hideAvatars: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
SpaceScaffoldRow(
modifier = modifier,
spaceRoom = spaceRoom,
onClick = onClick,
hideAvatars = hideAvatars,
onLongClick = { },
) {
NameAndIndicatorRow(
name = spaceRoom.name,
showIndicator = showUnreadIndicator,
)
Spacer(modifier = Modifier.height(1.dp))
if (!spaceRoom.worldReadable) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
modifier = Modifier
.size(16.dp)
.padding(end = 4.dp),
imageVector = CompoundIcons.LockSolid(),
contentDescription = null,
tint = ElementTheme.colors.iconTertiary,
)
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular,
text = stringResource(CommonStrings.common_private_space),
fontStyle = FontStyle.Italic.takeIf { spaceRoom.name == null },
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Spacer(modifier = Modifier.height(1.dp))
}
val spaceSummary = stringResource(
CommonStrings.screen_space_list_details,
pluralStringResource(CommonPlurals.common_rooms, spaceRoom.childrenCount, spaceRoom.childrenCount),
pluralStringResource(CommonPlurals.common_member_count, spaceRoom.numJoinedMembers, spaceRoom.numJoinedMembers),
)
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular,
text = spaceSummary,
fontStyle = FontStyle.Italic.takeIf { spaceRoom.name == null },
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (spaceRoom.state == CurrentUserMembership.INVITED) {
Spacer(modifier = Modifier.height(12.dp))
InviteButtonsRowMolecule(
onAcceptClick = {},
onDeclineClick = {},
)
}
}
}
@Composable
private fun NameAndIndicatorRow(
name: String?,
showIndicator: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyLgMedium,
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { name == null },
color = ElementTheme.colors.textPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (showIndicator) {
UnreadIndicatorAtom(
color = ElementTheme.colors.unreadIndicator
)
}
}
}
@Composable
private fun SpaceScaffoldRow(
spaceRoom: SpaceRoom,
onClick: () -> Unit,
onLongClick: () -> Unit,
hideAvatars: Boolean,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
val clickModifier = Modifier
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
onLongClickLabel = stringResource(CommonStrings.action_open_context_menu),
indication = ripple(),
interactionSource = remember { MutableInteractionSource() }
)
.onKeyboardContextMenuAction { onLongClick }
Row(
modifier = modifier
.fillMaxWidth()
.then(clickModifier)
.padding(horizontal = 16.dp, vertical = 8.dp)
.height(IntrinsicSize.Min),
) {
Avatar(
avatarData = spaceRoom.getAvatarData(AvatarSize.SpaceListItem),
avatarType = AvatarType.Space(),
hideImage = hideAvatars,
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f),
content = content,
)
}
}
@PreviewsDayNight
@Composable
internal fun HomeSpaceItemViewPreview(@PreviewParameter(SpaceRoomProvider::class) spaceRoom: SpaceRoom) = ElementPreview {
HomeSpaceItemView(
spaceRoom = spaceRoom,
showUnreadIndicator = false,
hideAvatars = true,
onClick = {},
)
}

View file

@ -0,0 +1,10 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
sealed interface HomeSpacesEvents

View file

@ -0,0 +1,54 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.seenSpaceIds
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.matrix.api.MatrixClient
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.flow.map
@Inject
class HomeSpacesPresenter(
private val client: MatrixClient,
private val seenInvitesStore: SeenInvitesStore,
) : Presenter<HomeSpacesState> {
@Composable
override fun present(): HomeSpacesState {
val hideInvitesAvatar by remember {
client
.mediaPreviewService()
.mediaPreviewConfigFlow
.mapState { config -> config.hideInviteAvatar }
}.collectAsState()
val spaceRooms by client.spaceService.spaceRooms.collectAsState(emptyList())
val seenSpaceInvites by remember {
seenInvitesStore.seenSpaceIds().map { it.toPersistentSet() }
}.collectAsState(persistentSetOf())
fun handleEvents(event: HomeSpacesEvents) {
// when (event) { }
}
return HomeSpacesState(
space = CurrentSpace.Root,
spaceRooms = spaceRooms,
seenSpaceInvites = seenSpaceInvites,
hideInvitesAvatar = hideInvitesAvatar,
eventSink = ::handleEvents,
)
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.collections.immutable.ImmutableSet
data class HomeSpacesState(
val space: CurrentSpace,
val spaceRooms: List<SpaceRoom>,
val seenSpaceInvites: ImmutableSet<SpaceId>,
val hideInvitesAvatar: Boolean,
val eventSink: (HomeSpacesEvents) -> Unit,
)
sealed interface CurrentSpace {
object Root : CurrentSpace
data class Space(val spaceRoom: SpaceRoom) : CurrentSpace
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import kotlinx.collections.immutable.toImmutableSet
open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
override val values: Sequence<HomeSpacesState>
get() = sequenceOf(
aHomeSpacesState(
spaceRooms = SpaceRoomProvider().values.toList(),
seenSpaceInvites = setOf(
SpaceId("!spaceId3:example.com"),
),
),
aHomeSpacesState(
space = CurrentSpace.Space(
spaceRoom = aSpaceRooms(spaceId = SpaceId("!mySpace:example.com"))
),
spaceRooms = aListOfSpaceRooms(),
),
)
}
internal fun aHomeSpacesState(
space: CurrentSpace = CurrentSpace.Root,
spaceRooms: List<SpaceRoom> = aListOfSpaceRooms(),
seenSpaceInvites: Set<SpaceId> = emptySet(),
hideInvitesAvatar: Boolean = false,
eventSink: (HomeSpacesEvents) -> Unit = {},
) = HomeSpacesState(
space = space,
spaceRooms = spaceRooms,
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
hideInvitesAvatar = hideInvitesAvatar,
eventSink = eventSink,
)
fun aListOfSpaceRooms(): List<SpaceRoom> {
return listOf(
aSpaceRooms(spaceId = SpaceId("!spaceId0:example.com")),
aSpaceRooms(spaceId = SpaceId("!spaceId1:example.com")),
aSpaceRooms(spaceId = SpaceId("!spaceId2:example.com")),
)
}

View file

@ -0,0 +1,80 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
import io.element.android.libraries.matrix.ui.model.getAvatarData
import kotlinx.collections.immutable.toImmutableList
@Composable
fun HomeSpacesView(
state: HomeSpacesState,
onSpaceClick: (SpaceId) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier) {
val space = state.space
when (space) {
CurrentSpace.Root -> {
item {
SpaceHeaderRootView(
numberOfSpaces = state.spaceRooms.size,
// TODO
numberOfRooms = 0,
)
}
}
is CurrentSpace.Space -> item {
SpaceHeaderView(
avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader),
name = space.spaceRoom.name,
topic = space.spaceRoom.topic,
joinRule = space.spaceRoom.joinRule,
heroes = space.spaceRoom.heroes.toImmutableList(),
numberOfMembers = space.spaceRoom.numJoinedMembers,
numberOfRooms = space.spaceRoom.childrenCount,
)
}
}
state.spaceRooms.forEach {
item(it.spaceId) {
val isInvitation = it.state == CurrentUserMembership.INVITED
HomeSpaceItemView(
spaceRoom = it,
showUnreadIndicator = isInvitation && it.spaceId !in state.seenSpaceInvites,
hideAvatars = isInvitation && state.hideInvitesAvatar,
onClick = {
onSpaceClick(it.spaceId)
}
)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun HomeSpacesViewPreview(
@PreviewParameter(HomeSpacesStateProvider::class) state: HomeSpacesState,
) = ElementPreview {
HomeSpacesView(
state = state,
onSpaceClick = {},
modifier = Modifier,
)
}

View file

@ -0,0 +1,84 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.api.user.MatrixUser
class SpaceRoomProvider : PreviewParameterProvider<SpaceRoom> {
override val values: Sequence<SpaceRoom> = sequenceOf(
aSpaceRooms(),
aSpaceRooms(
numJoinedMembers = 5,
childrenCount = 10,
worldReadable = true,
spaceId = SpaceId("!spaceId0:example.com"),
),
aSpaceRooms(
numJoinedMembers = 5,
childrenCount = 10,
worldReadable = true,
avatarUrl = "anUrl",
spaceId = SpaceId("!spaceId1:example.com"),
),
aSpaceRooms(
name = null,
numJoinedMembers = 5,
childrenCount = 10,
worldReadable = true,
avatarUrl = "anUrl",
spaceId = SpaceId("!spaceId2:example.com"),
state = CurrentUserMembership.INVITED,
),
aSpaceRooms(
name = null,
numJoinedMembers = 5,
childrenCount = 10,
worldReadable = true,
avatarUrl = "anUrl",
spaceId = SpaceId("!spaceId3:example.com"),
state = CurrentUserMembership.INVITED,
),
)
}
fun aSpaceRooms(
name: String? = "Space name",
avatarUrl: String? = null,
canonicalAlias: RoomAlias? = null,
childrenCount: Int = 0,
guestCanJoin: Boolean = false,
heroes: List<MatrixUser> = emptyList(),
joinRule: JoinRule? = null,
numJoinedMembers: Int = 0,
spaceId: SpaceId = SpaceId("!spaceId:example.com"),
roomType: RoomType = RoomType.Space,
state: CurrentUserMembership? = null,
topic: String? = null,
worldReadable: Boolean = false,
) = SpaceRoom(
name = name,
avatarUrl = avatarUrl,
canonicalAlias = canonicalAlias,
childrenCount = childrenCount,
guestCanJoin = guestCanJoin,
heroes = heroes,
joinRule = joinRule,
numJoinedMembers = numJoinedMembers,
spaceId = spaceId,
roomType = roomType,
state = state,
topic = topic,
worldReadable = worldReadable
)

View file

@ -12,6 +12,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.home.impl.roomlist.aRoomListState
import io.element.android.features.home.impl.spaces.aHomeSpacesState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@ -158,6 +159,7 @@ class HomePresenterTest {
indicatorService = indicatorService,
logoutPresenter = { aDirectLogoutState() },
roomListPresenter = { aRoomListState() },
homeSpacesPresenter = { aHomeSpacesState() },
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
featureFlagService = featureFlagService,
)

View file

@ -0,0 +1,39 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.home.impl.spaces
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest
import org.junit.Test
class HomeSpacesPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = createPresenter()
presenter.test {
val state = awaitItem()
assertThat(state.space).isEqualTo(CurrentSpace.Root)
assertThat(state.spaceRooms).isEmpty()
assertThat(state.hideInvitesAvatar).isFalse()
assertThat(state.seenSpaceInvites).isEmpty()
}
}
private fun createPresenter(
client: MatrixClient = FakeMatrixClient(),
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
) = HomeSpacesPresenter(
client = client,
seenInvitesStore = seenInvitesStore,
)
}

View file

@ -8,7 +8,10 @@
package io.element.android.features.invite.api
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SpaceId
import io.element.android.libraries.matrix.api.core.toSpaceId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
interface SeenInvitesStore {
/**
@ -35,3 +38,9 @@ interface SeenInvitesStore {
*/
suspend fun clear()
}
fun SeenInvitesStore.seenSpaceIds(): Flow<Set<SpaceId>> {
return seenRoomIds().map { roomIds ->
roomIds.map { it.toSpaceId() }.toSet()
}
}

View file

@ -546,7 +546,7 @@ private fun DefaultLoadedContent(
},
memberCount = {
if (contentState.showMemberCount) {
MembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
MembersCountMolecule(memberCount = contentState.numberOfMembers?.toInt() ?: 0)
}
}
)