Space List
Extract InviteButtonsRowMolecule Take into account seenSpaceInvites
This commit is contained in:
parent
26329aa6a8
commit
85d7532124
24 changed files with 811 additions and 40 deletions
|
|
@ -18,6 +18,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
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.logout.api.direct.DirectLogoutState
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -36,6 +37,7 @@ class HomePresenter(
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
private val indicatorService: IndicatorService,
|
private val indicatorService: IndicatorService,
|
||||||
private val roomListPresenter: Presenter<RoomListState>,
|
private val roomListPresenter: Presenter<RoomListState>,
|
||||||
|
private val homeSpacesPresenter: Presenter<HomeSpacesState>,
|
||||||
private val logoutPresenter: Presenter<DirectLogoutState>,
|
private val logoutPresenter: Presenter<DirectLogoutState>,
|
||||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
|
@ -46,6 +48,7 @@ class HomePresenter(
|
||||||
val isOnline by syncService.isOnline.collectAsState()
|
val isOnline by syncService.isOnline.collectAsState()
|
||||||
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
||||||
val roomListState = roomListPresenter.present()
|
val roomListState = roomListPresenter.present()
|
||||||
|
val homeSpacesState = homeSpacesPresenter.present()
|
||||||
val isSpaceFeatureEnabled by remember {
|
val isSpaceFeatureEnabled by remember {
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
|
||||||
}.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
|
|
@ -78,6 +81,7 @@ class HomePresenter(
|
||||||
hasNetworkConnection = isOnline,
|
hasNetworkConnection = isOnline,
|
||||||
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||||
roomListState = roomListState,
|
roomListState = roomListState,
|
||||||
|
homeSpacesState = homeSpacesState,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
directLogoutState = directLogoutState,
|
directLogoutState = directLogoutState,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.home.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
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.logout.api.direct.DirectLogoutState
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
@ -20,6 +21,7 @@ data class HomeState(
|
||||||
val hasNetworkConnection: Boolean,
|
val hasNetworkConnection: Boolean,
|
||||||
val currentHomeNavigationBarItem: HomeNavigationBarItem,
|
val currentHomeNavigationBarItem: HomeNavigationBarItem,
|
||||||
val roomListState: RoomListState,
|
val roomListState: RoomListState,
|
||||||
|
val homeSpacesState: HomeSpacesState,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val canReportBug: Boolean,
|
val canReportBug: Boolean,
|
||||||
val directLogoutState: DirectLogoutState,
|
val directLogoutState: DirectLogoutState,
|
||||||
|
|
|
||||||
|
|
@ -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.aRoomListState
|
||||||
import io.element.android.features.home.impl.roomlist.aRoomsContentState
|
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.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.DirectLogoutState
|
||||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
|
|
@ -51,6 +53,7 @@ internal fun aHomeState(
|
||||||
snackbarMessage: SnackbarMessage? = null,
|
snackbarMessage: SnackbarMessage? = null,
|
||||||
currentHomeNavigationBarItem: HomeNavigationBarItem = HomeNavigationBarItem.Chats,
|
currentHomeNavigationBarItem: HomeNavigationBarItem = HomeNavigationBarItem.Chats,
|
||||||
roomListState: RoomListState = aRoomListState(),
|
roomListState: RoomListState = aRoomListState(),
|
||||||
|
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
|
||||||
canReportBug: Boolean = true,
|
canReportBug: Boolean = true,
|
||||||
isSpaceFeatureEnabled: Boolean = false,
|
isSpaceFeatureEnabled: Boolean = false,
|
||||||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
||||||
|
|
@ -64,6 +67,7 @@ internal fun aHomeState(
|
||||||
directLogoutState = directLogoutState,
|
directLogoutState = directLogoutState,
|
||||||
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||||
roomListState = roomListState,
|
roomListState = roomListState,
|
||||||
|
homeSpacesState = homeSpacesState,
|
||||||
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
|
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
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.RoomListEvents
|
||||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
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.search.RoomListSearchView
|
||||||
|
import io.element.android.features.home.impl.spaces.HomeSpacesView
|
||||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
|
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
|
||||||
import io.element.android.libraries.androidutils.throttler.FirstThrottler
|
import io.element.android.libraries.androidutils.throttler.FirstThrottler
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
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.NavigationBarItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.NavigationBarText
|
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.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.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -262,19 +261,17 @@ private fun HomeScaffold(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
HomeNavigationBarItem.Spaces -> {
|
HomeNavigationBarItem.Spaces -> {
|
||||||
Box(
|
HomeSpacesView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(padding)
|
||||||
) {
|
.hazeSource(state = hazeState),
|
||||||
Text(
|
state = state.homeSpacesState,
|
||||||
modifier = Modifier.align(Alignment.Center),
|
onSpaceClick = { spaceId ->
|
||||||
text = "Spaces are coming soon!",
|
// TODO
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
}
|
||||||
color = ElementTheme.colors.textPrimary,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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.features.home.impl.roomlist.RoomListEvents
|
||||||
import io.element.android.libraries.core.extensions.orEmpty
|
import io.element.android.libraries.core.extensions.orEmpty
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
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.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||||
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
|
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
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.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.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.roomListRoomMessage
|
import io.element.android.libraries.designsystem.theme.roomListRoomMessage
|
||||||
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
|
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
|
||||||
|
|
@ -101,7 +99,7 @@ internal fun RoomSummaryRow(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
InviteButtonsRow(
|
InviteButtonsRowMolecule(
|
||||||
onAcceptClick = {
|
onAcceptClick = {
|
||||||
eventSink(RoomListEvents.AcceptInvite(room))
|
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
|
@Composable
|
||||||
private fun OnGoingCallIcon(
|
private fun OnGoingCallIcon(
|
||||||
color: Color,
|
color: Color,
|
||||||
|
|
|
||||||
|
|
@ -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<HomeSpacesState>
|
||||||
|
}
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<HomeSpacesState> {
|
||||||
|
@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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SpaceRoom>,
|
||||||
|
val seenSpaceInvites: ImmutableSet<SpaceId>,
|
||||||
|
val eventSink: (HomeSpacesEvents) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -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<HomeSpacesState> {
|
||||||
|
override val values: Sequence<HomeSpacesState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aHomeSpacesState(
|
||||||
|
spaceRooms = SpaceRoomProvider().values.toList(),
|
||||||
|
seenSpaceInvites = setOf(
|
||||||
|
SpaceId("!spaceId3:example.com"),
|
||||||
|
).toImmutableSet(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun aHomeSpacesState(
|
||||||
|
spaceRooms: List<SpaceRoom> = aListOfSpaceRooms(),
|
||||||
|
seenSpaceInvites: Set<SpaceId> = emptySet(),
|
||||||
|
eventSink: (HomeSpacesEvents) -> Unit = {},
|
||||||
|
) = HomeSpacesState(
|
||||||
|
spaceRooms = spaceRooms,
|
||||||
|
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aListOfSpaceRooms(): List<SpaceRoom> {
|
||||||
|
return listOf(
|
||||||
|
aSpaceRooms(),
|
||||||
|
aSpaceRooms(),
|
||||||
|
aSpaceRooms(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -8,7 +8,10 @@
|
||||||
package io.element.android.features.invite.api
|
package io.element.android.features.invite.api
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
interface SeenInvitesStore {
|
interface SeenInvitesStore {
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,3 +38,9 @@ interface SeenInvitesStore {
|
||||||
*/
|
*/
|
||||||
suspend fun clear()
|
suspend fun clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun SeenInvitesStore.seenSpaceIds(): Flow<Set<SpaceId>> {
|
||||||
|
return seenRoomIds().map { roomIds ->
|
||||||
|
roomIds.map { it.toSpaceId() }.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -17,6 +17,8 @@ enum class AvatarSize(val dp: Dp) {
|
||||||
RoomHeader(96.dp),
|
RoomHeader(96.dp),
|
||||||
RoomListItem(52.dp),
|
RoomListItem(52.dp),
|
||||||
|
|
||||||
|
SpaceListItem(52.dp),
|
||||||
|
|
||||||
RoomSelectRoomListItem(36.dp),
|
RoomSelectRoomListItem(36.dp),
|
||||||
|
|
||||||
UserPreference(56.dp),
|
UserPreference(56.dp),
|
||||||
|
|
|
||||||
|
|
@ -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.room.alias.ResolvedRoomAlias
|
||||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
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.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.SlidingSyncVersion
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
|
||||||
|
|
@ -47,6 +48,7 @@ interface MatrixClient {
|
||||||
val deviceId: DeviceId
|
val deviceId: DeviceId
|
||||||
val userProfile: StateFlow<MatrixUser>
|
val userProfile: StateFlow<MatrixUser>
|
||||||
val roomListService: RoomListService
|
val roomListService: RoomListService
|
||||||
|
val spaceService: SpaceService
|
||||||
val mediaLoader: MatrixMediaLoader
|
val mediaLoader: MatrixMediaLoader
|
||||||
val sessionCoroutineScope: CoroutineScope
|
val sessionCoroutineScope: CoroutineScope
|
||||||
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
|
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,5 @@ value class RoomId(val value: String) : Serializable {
|
||||||
|
|
||||||
override fun toString(): String = value
|
override fun toString(): String = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun RoomId.toSpaceId(): SpaceId = SpaceId(this.value)
|
||||||
|
|
|
||||||
|
|
@ -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<MatrixUser>,
|
||||||
|
val joinRule: JoinRule?,
|
||||||
|
val numJoinedMembers: Int,
|
||||||
|
val spaceId: SpaceId,
|
||||||
|
val roomType: RoomType,
|
||||||
|
val state: CurrentUserMembership?,
|
||||||
|
val topic: String?,
|
||||||
|
val worldReadable: Boolean,
|
||||||
|
)
|
||||||
|
|
@ -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<List<SpaceRoom>>
|
||||||
|
suspend fun joinedSpaces(): Result<List<SpaceRoom>>
|
||||||
|
}
|
||||||
|
|
@ -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.RoomDirectoryService
|
||||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
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.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.SlidingSyncVersion
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
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.RoomListFactory
|
||||||
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
|
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.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.RustSyncService
|
||||||
import io.element.android.libraries.matrix.impl.sync.map
|
import io.element.android.libraries.matrix.impl.sync.map
|
||||||
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
|
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
|
||||||
|
|
@ -143,6 +145,7 @@ class RustMatrixClient(
|
||||||
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
|
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
|
||||||
|
|
||||||
private val innerRoomListService = innerSyncService.roomListService()
|
private val innerRoomListService = innerSyncService.roomListService()
|
||||||
|
private val innerSpaceService = innerClient.spaceService()
|
||||||
|
|
||||||
private val rustSyncService = RustSyncService(
|
private val rustSyncService = RustSyncService(
|
||||||
inner = innerSyncService,
|
inner = innerSyncService,
|
||||||
|
|
@ -184,6 +187,17 @@ class RustMatrixClient(
|
||||||
roomSyncSubscriber = roomSyncSubscriber,
|
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(
|
private val verificationService = RustSessionVerificationService(
|
||||||
client = innerClient,
|
client = innerClient,
|
||||||
isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running },
|
isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running },
|
||||||
|
|
@ -540,6 +554,7 @@ class RustMatrixClient(
|
||||||
|
|
||||||
sessionDelegate.clearCurrentClient()
|
sessionDelegate.clearCurrentClient()
|
||||||
innerRoomListService.close()
|
innerRoomListService.close()
|
||||||
|
innerSpaceService.close()
|
||||||
notificationService.close()
|
notificationService.close()
|
||||||
encryptionService.close()
|
encryptionService.close()
|
||||||
innerClient.close()
|
innerClient.close()
|
||||||
|
|
|
||||||
|
|
@ -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<List<SpaceRoom>>(replay = 1, extraBufferCapacity = 1)
|
||||||
|
|
||||||
|
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
|
||||||
|
runCatchingExceptions {
|
||||||
|
innerSpaceService.joinedSpaces()
|
||||||
|
.map {
|
||||||
|
it.let(mapper::map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// override suspend fun spaceRoomList(spaceId: SpaceId): Result<List<SpaceRoom>> = withContext(sessionDispatcher) {
|
||||||
|
// runCatchingExceptions {
|
||||||
|
// innerSpaceService.spaceRoomList(spaceId.value)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
init {
|
||||||
|
innerSpaceService
|
||||||
|
.spaceDiffFlow()
|
||||||
|
.onEach {
|
||||||
|
handeUpdate(it)
|
||||||
|
}
|
||||||
|
.launchIn(sessionCoroutineScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handeUpdate(spaceListUpdates: List<SpaceListUpdate>) {
|
||||||
|
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<SpaceRoom>.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<List<SpaceListUpdate>> =
|
||||||
|
callbackFlow {
|
||||||
|
val listener = object : SpaceServiceJoinedSpacesListener {
|
||||||
|
override fun onUpdate(roomUpdates: List<SpaceListUpdate>) {
|
||||||
|
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)
|
||||||
|
|
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue