diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt index f984bcbc42..f0debd88ab 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomePresenter.kt @@ -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, + private val homeSpacesPresenter: Presenter, private val logoutPresenter: Presenter, 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, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt index 5e6c16d2e4..d8668e3200 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeState.kt @@ -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, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt index 7a50296e17..1f78053c0a 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt @@ -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, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt index a0468aa32d..9217efcfe5 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt @@ -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 + } + ) } } }, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index 75ec727055..3036865eea 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -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, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesModule.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesModule.kt new file mode 100644 index 0000000000..aee2ad6b8f --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesModule.kt @@ -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 com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +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 + +@ContributesTo(SessionScope::class) +@Module +interface HomeSpacesModule { + @Binds + fun bindHomeSpacesPresenter(presenter: HomeSpacesPresenter): Presenter +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpaceItemView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpaceItemView.kt new file mode 100644 index 0000000000..3b0b88a9a2 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpaceItemView.kt @@ -0,0 +1,194 @@ +/* + * 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, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + SpaceScaffoldRow( + modifier = modifier, + spaceRoom = spaceRoom, + onClick = onClick, + 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 +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, + modifier: Modifier = Modifier, + hideAvatarImage: Boolean, + 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 = hideAvatarImage, + ) + 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, + onClick = {}, + ) +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvents.kt new file mode 100644 index 0000000000..dab1d05944 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvents.kt @@ -0,0 +1,12 @@ +/* + * 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 { + +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt new file mode 100644 index 0000000000..bdd89257f0 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt @@ -0,0 +1,44 @@ +/* + * 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 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.matrix.api.MatrixClient +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toPersistentSet +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class HomeSpacesPresenter @Inject constructor( + private val client: MatrixClient, + private val seenInvitesStore: SeenInvitesStore, +) : Presenter { + @Composable + override fun present(): HomeSpacesState { + 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( + spaceRooms = spaceRooms, + seenSpaceInvites = seenSpaceInvites, + eventSink = ::handleEvents, + ) + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt new file mode 100644 index 0000000000..cc4bbd7375 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt @@ -0,0 +1,18 @@ +/* + * 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 spaceRooms: List, + val seenSpaceInvites: ImmutableSet, + val eventSink: (HomeSpacesEvents) -> Unit, +) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt new file mode 100644 index 0000000000..546b4506ff --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt @@ -0,0 +1,43 @@ +/* + * 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 { + override val values: Sequence + get() = sequenceOf( + aHomeSpacesState( + spaceRooms = SpaceRoomProvider().values.toList(), + seenSpaceInvites = setOf( + SpaceId("!spaceId3:example.com"), + ).toImmutableSet(), + ) + ) +} + +internal fun aHomeSpacesState( + spaceRooms: List = aListOfSpaceRooms(), + seenSpaceInvites: Set = emptySet(), + eventSink: (HomeSpacesEvents) -> Unit = {}, +) = HomeSpacesState( + spaceRooms = spaceRooms, + seenSpaceInvites = seenSpaceInvites.toImmutableSet(), + eventSink = eventSink, +) + +fun aListOfSpaceRooms(): List { + return listOf( + aSpaceRooms(), + aSpaceRooms(), + aSpaceRooms(), + ) +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt new file mode 100644 index 0000000000..9924105013 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt @@ -0,0 +1,50 @@ +/* + * 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.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 + +@Composable +fun HomeSpacesView( + state: HomeSpacesState, + onSpaceClick: (SpaceId) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier) { + state.spaceRooms.forEach { + item(it.spaceId) { + HomeSpaceItemView( + spaceRoom = it, + showUnreadIndicator = it.state == CurrentUserMembership.INVITED && it.spaceId !in state.seenSpaceInvites, + onClick = { + onSpaceClick(it.spaceId) + } + ) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun HomeSpacesViewPreview( + @PreviewParameter(HomeSpacesStateProvider::class) state: HomeSpacesState, +) = ElementPreview { + HomeSpacesView( + state = state, + onSpaceClick = {}, + modifier = Modifier, + ) +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt new file mode 100644 index 0000000000..29c18c78a0 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt @@ -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 { + override val values: Sequence = 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 = 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 +) diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt index 682970ffe7..6c609e3810 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt @@ -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> { + return seenRoomIds().map { roomIds -> + roomIds.map { it.toSpaceId() }.toSet() + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InviteButtonsRowMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InviteButtonsRowMolecule.kt new file mode 100644 index 0000000000..62346fb092 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InviteButtonsRowMolecule.kt @@ -0,0 +1,47 @@ +/* + * 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.libraries.designsystem.atomic.molecules + +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +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.OutlinedButton +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun InviteButtonsRowMolecule( + onAcceptClick: () -> Unit, + onDeclineClick: () -> Unit, + modifier: Modifier = Modifier, + declineText: String = stringResource(CommonStrings.action_decline), + acceptText: String = stringResource(CommonStrings.action_accept), +) { + Row( + modifier = modifier, + horizontalArrangement = spacedBy(12.dp) + ) { + OutlinedButton( + text = declineText, + onClick = onDeclineClick, + size = ButtonSize.MediumLowPadding, + modifier = Modifier.weight(1f), + ) + Button( + text = acceptText, + onClick = onAcceptClick, + size = ButtonSize.MediumLowPadding, + modifier = Modifier.weight(1f), + ) + } +} + diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 91d52cfc84..9e9ebb6181 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -17,6 +17,8 @@ enum class AvatarSize(val dp: Dp) { RoomHeader(96.dp), RoomListItem(52.dp), + SpaceListItem(52.dp), + RoomSelectRoomListItem(36.dp), UserPreference(56.dp), diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 3c0f95745e..bc7ec21152 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias 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.spaces.SpaceService import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults @@ -47,6 +48,7 @@ interface MatrixClient { val deviceId: DeviceId val userProfile: StateFlow val roomListService: RoomListService + val spaceService: SpaceService val mediaLoader: MatrixMediaLoader val sessionCoroutineScope: CoroutineScope val ignoredUsersFlow: StateFlow> diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt index 8d339ae704..1fab64020d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt @@ -20,3 +20,5 @@ value class RoomId(val value: String) : Serializable { override fun toString(): String = value } + +fun RoomId.toSpaceId(): SpaceId = SpaceId(this.value) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt new file mode 100644 index 0000000000..ce697089c7 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoom.kt @@ -0,0 +1,31 @@ +/* + * 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.libraries.matrix.api.spaces + +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.user.MatrixUser + +data class SpaceRoom( + val name: String?, + val avatarUrl: String?, + val canonicalAlias: RoomAlias?, + val childrenCount: Int, + val guestCanJoin: Boolean, + val heroes: List, + val joinRule: JoinRule?, + val numJoinedMembers: Int, + val spaceId: SpaceId, + val roomType: RoomType, + val state: CurrentUserMembership?, + val topic: String?, + val worldReadable: Boolean, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt new file mode 100644 index 0000000000..58494502f7 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt @@ -0,0 +1,15 @@ +/* + * 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.libraries.matrix.api.spaces + +import kotlinx.coroutines.flow.SharedFlow + +interface SpaceService { + val spaceRooms: SharedFlow> + suspend fun joinedSpaces(): Result> +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 85c1dde7e2..f4b64daf59 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -42,6 +42,7 @@ import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState @@ -71,6 +72,7 @@ import io.element.android.libraries.matrix.impl.roomdirectory.map 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.roomOrNull +import io.element.android.libraries.matrix.impl.spaces.RustSpaceService import io.element.android.libraries.matrix.impl.sync.RustSyncService import io.element.android.libraries.matrix.impl.sync.map import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper @@ -143,6 +145,7 @@ class RustMatrixClient( private val sessionDispatcher = dispatchers.io.limitedParallelism(64) private val innerRoomListService = innerSyncService.roomListService() + private val innerSpaceService = innerClient.spaceService() private val rustSyncService = RustSyncService( inner = innerSyncService, @@ -184,6 +187,17 @@ class RustMatrixClient( roomSyncSubscriber = roomSyncSubscriber, ) + override val spaceService: SpaceService = RustSpaceService( + innerSpaceService = innerSpaceService, + sessionCoroutineScope = sessionCoroutineScope, + sessionDispatcher = sessionDispatcher, + //roomListFactory = RoomListFactory( + // innerRoomListService = innerRoomListService, + // sessionCoroutineScope = sessionCoroutineScope, + //), + //roomSyncSubscriber = roomSyncSubscriber, + ) + private val verificationService = RustSessionVerificationService( client = innerClient, isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running }, @@ -540,6 +554,7 @@ class RustMatrixClient( sessionDelegate.clearCurrentClient() innerRoomListService.close() + innerSpaceService.close() notificationService.close() encryptionService.close() innerClient.close() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt new file mode 100644 index 0000000000..03367daae1 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/RustSpaceService.kt @@ -0,0 +1,145 @@ +/* + * 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.libraries.matrix.impl.spaces + +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.spaces.SpaceRoom +import io.element.android.libraries.matrix.api.spaces.SpaceService +import io.element.android.libraries.matrix.impl.util.cancelAndDestroy +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.SpaceListUpdate +import org.matrix.rustcomponents.sdk.SpaceServiceInterface +import org.matrix.rustcomponents.sdk.SpaceServiceJoinedSpacesListener +import timber.log.Timber +import org.matrix.rustcomponents.sdk.SpaceService as ClientSpaceService + +class RustSpaceService( + private val innerSpaceService: ClientSpaceService, + private val sessionCoroutineScope: CoroutineScope, + private val sessionDispatcher: CoroutineDispatcher, +) : SpaceService { + private val mapper = SpaceRoomMapper() + private val mutex = Mutex() + + override val spaceRooms = MutableSharedFlow>(replay = 1, extraBufferCapacity = 1) + + override suspend fun joinedSpaces(): Result> = withContext(sessionDispatcher) { + runCatchingExceptions { + innerSpaceService.joinedSpaces() + .map { + it.let(mapper::map) + } + } + } + + // override suspend fun spaceRoomList(spaceId: SpaceId): Result> = withContext(sessionDispatcher) { + // runCatchingExceptions { + // innerSpaceService.spaceRoomList(spaceId.value) + // } + // } + + init { + innerSpaceService + .spaceDiffFlow() + .onEach { + handeUpdate(it) + } + .launchIn(sessionCoroutineScope) + } + + private suspend fun handeUpdate(spaceListUpdates: List) { + mutex.withLock { + val current = if (spaceRooms.replayCache.isNotEmpty()) { + spaceRooms.first().toMutableList() + } else { + mutableListOf() + } + spaceListUpdates.forEach { update -> + current.applyUpdate(update) + } + spaceRooms.emit(current) + } + } + + private fun MutableList.applyUpdate(update: SpaceListUpdate) { + when (update) { + is SpaceListUpdate.Append -> { + val newSpaces = update.values.map { it -> + it.let(mapper::map) + } + addAll(newSpaces) + } + SpaceListUpdate.Clear -> clear() + is SpaceListUpdate.Insert -> { + val newSpace = mapper.map(update.value) + add(update.index.toInt(), newSpace) + } + SpaceListUpdate.PopBack -> { + removeAt(lastIndex) + } + SpaceListUpdate.PopFront -> { + removeAt(0) + } + is SpaceListUpdate.PushBack -> { + val newSpace = mapper.map(update.value) + add(newSpace) + } + is SpaceListUpdate.PushFront -> { + val newSpace = mapper.map(update.value) + add(0, newSpace) + } + is SpaceListUpdate.Remove -> { + removeAt(update.index.toInt()) + } + is SpaceListUpdate.Reset -> { + clear() + val newSpaces = update.values.map(mapper::map) + addAll(newSpaces) + } + is SpaceListUpdate.Set -> { + val newSpace = mapper.map(update.value) + this[update.index.toInt()] = newSpace + } + is SpaceListUpdate.Truncate -> { + subList(update.length.toInt(), size).clear() + } + } + } +} + +internal fun SpaceServiceInterface.spaceDiffFlow(): Flow> = + callbackFlow { + val listener = object : SpaceServiceJoinedSpacesListener { + override fun onUpdate(roomUpdates: List) { + trySendBlocking(roomUpdates) + } + } + Timber.d("Open spaceDiffFlow for SpaceServiceInterface ${this@spaceDiffFlow}") + val taskHandle = subscribeToJoinedSpaces(listener) + awaitClose { + Timber.d("Close spaceDiffFlow for SpaceServiceInterface ${this@spaceDiffFlow}") + taskHandle.cancelAndDestroy() + } + }.catch { + Timber.d(it, "spaceDiffFlow() failed") + }.buffer(Channel.UNLIMITED) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt new file mode 100644 index 0000000000..5622ee2c8c --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/spaces/SpaceRoomMapper.kt @@ -0,0 +1,36 @@ +/* + * 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.libraries.matrix.impl.spaces + +import io.element.android.libraries.core.bool.orFalse +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.spaces.SpaceRoom +import io.element.android.libraries.matrix.impl.room.join.map +import io.element.android.libraries.matrix.impl.room.map +import org.matrix.rustcomponents.sdk.SpaceRoom as RustSpaceRoom + +class SpaceRoomMapper { + fun map(spaceRoom: RustSpaceRoom): SpaceRoom { + return SpaceRoom( + avatarUrl = spaceRoom.avatarUrl, + canonicalAlias = spaceRoom.canonicalAlias?.let(::RoomAlias), + childrenCount = spaceRoom.childrenCount.toInt(), + guestCanJoin = spaceRoom.guestCanJoin, + heroes = spaceRoom.heroes.orEmpty().map { it.map() }, + joinRule = spaceRoom.joinRule?.map(), + name = spaceRoom.name, + numJoinedMembers = spaceRoom.numJoinedMembers.toInt(), + spaceId = spaceRoom.roomId.let(::SpaceId), + roomType = spaceRoom.roomType.map(), + state = spaceRoom.state?.map(), + topic = spaceRoom.topic, + worldReadable = spaceRoom.worldReadable.orFalse(), + ) + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SpaceExtension.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SpaceExtension.kt new file mode 100644 index 0000000000..afe03b6877 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/SpaceExtension.kt @@ -0,0 +1,19 @@ +/* + * 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.libraries.matrix.ui.model + +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.spaces.SpaceRoom + +fun SpaceRoom.getAvatarData(size: AvatarSize) = AvatarData( + id = spaceId.value, + name = name, + url = avatarUrl, + size = size, +)