Merge pull request #5273 from element-hq/feature/bma/spaceNextStep
Space: add content in home screen
This commit is contained in:
commit
a2dd455f22
146 changed files with 1298 additions and 250 deletions
|
|
@ -18,6 +18,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.home.impl.roomlist.RoomListState
|
||||
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -36,6 +37,7 @@ class HomePresenter(
|
|||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
private val indicatorService: IndicatorService,
|
||||
private val roomListPresenter: Presenter<RoomListState>,
|
||||
private val homeSpacesPresenter: Presenter<HomeSpacesState>,
|
||||
private val logoutPresenter: Presenter<DirectLogoutState>,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
|
|
@ -46,6 +48,7 @@ class HomePresenter(
|
|||
val isOnline by syncService.isOnline.collectAsState()
|
||||
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
||||
val roomListState = roomListPresenter.present()
|
||||
val homeSpacesState = homeSpacesPresenter.present()
|
||||
val isSpaceFeatureEnabled by remember {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
|
||||
}.collectAsState(initial = false)
|
||||
|
|
@ -78,6 +81,7 @@ class HomePresenter(
|
|||
hasNetworkConnection = isOnline,
|
||||
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||
roomListState = roomListState,
|
||||
homeSpacesState = homeSpacesState,
|
||||
snackbarMessage = snackbarMessage,
|
||||
canReportBug = canReportBug,
|
||||
directLogoutState = directLogoutState,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.home.impl.di
|
||||
|
||||
import dev.zacsweers.metro.BindingContainer
|
||||
import dev.zacsweers.metro.Binds
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import io.element.android.features.home.impl.spaces.HomeSpacesPresenter
|
||||
import io.element.android.features.home.impl.spaces.HomeSpacesState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@BindingContainer
|
||||
@ContributesTo(SessionScope::class)
|
||||
interface HomeSpacesModule {
|
||||
@Binds
|
||||
fun bindHomeSpacesPresenter(presenter: HomeSpacesPresenter): Presenter<HomeSpacesState>
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.home.impl.spaces
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
internal fun HomeSpaceItemView(
|
||||
spaceRoom: SpaceRoom,
|
||||
showUnreadIndicator: Boolean,
|
||||
hideAvatars: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SpaceScaffoldRow(
|
||||
modifier = modifier,
|
||||
spaceRoom = spaceRoom,
|
||||
onClick = onClick,
|
||||
hideAvatars = hideAvatars,
|
||||
onLongClick = { },
|
||||
) {
|
||||
NameAndIndicatorRow(
|
||||
name = spaceRoom.name,
|
||||
showIndicator = showUnreadIndicator,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(1.dp))
|
||||
if (!spaceRoom.worldReadable) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.padding(end = 4.dp),
|
||||
imageVector = CompoundIcons.LockSolid(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconTertiary,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
text = stringResource(CommonStrings.common_private_space),
|
||||
fontStyle = FontStyle.Italic.takeIf { spaceRoom.name == null },
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(1.dp))
|
||||
}
|
||||
val spaceSummary = stringResource(
|
||||
CommonStrings.screen_space_list_details,
|
||||
pluralStringResource(CommonPlurals.common_rooms, spaceRoom.childrenCount, spaceRoom.childrenCount),
|
||||
pluralStringResource(CommonPlurals.common_member_count, spaceRoom.numJoinedMembers, spaceRoom.numJoinedMembers),
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
text = spaceSummary,
|
||||
fontStyle = FontStyle.Italic.takeIf { spaceRoom.name == null },
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (spaceRoom.state == CurrentUserMembership.INVITED) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
InviteButtonsRowMolecule(
|
||||
onAcceptClick = {},
|
||||
onDeclineClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NameAndIndicatorRow(
|
||||
name: String?,
|
||||
showIndicator: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
text = name ?: stringResource(id = CommonStrings.common_no_room_name),
|
||||
fontStyle = FontStyle.Italic.takeIf { name == null },
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (showIndicator) {
|
||||
UnreadIndicatorAtom(
|
||||
color = ElementTheme.colors.unreadIndicator
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SpaceScaffoldRow(
|
||||
spaceRoom: SpaceRoom,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
hideAvatars: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
val clickModifier = Modifier
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
onLongClickLabel = stringResource(CommonStrings.action_open_context_menu),
|
||||
indication = ripple(),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
.onKeyboardContextMenuAction { onLongClick }
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(clickModifier)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = spaceRoom.getAvatarData(AvatarSize.SpaceListItem),
|
||||
avatarType = AvatarType.Space(),
|
||||
hideImage = hideAvatars,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun HomeSpaceItemViewPreview(@PreviewParameter(SpaceRoomProvider::class) spaceRoom: SpaceRoom) = ElementPreview {
|
||||
HomeSpaceItemView(
|
||||
spaceRoom = spaceRoom,
|
||||
showUnreadIndicator = false,
|
||||
hideAvatars = true,
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.home.impl.spaces
|
||||
|
||||
sealed interface HomeSpacesEvents
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.home.impl.spaces
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.features.invite.api.seenSpaceIds
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.mapState
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@Inject
|
||||
class HomeSpacesPresenter(
|
||||
private val client: MatrixClient,
|
||||
private val seenInvitesStore: SeenInvitesStore,
|
||||
) : Presenter<HomeSpacesState> {
|
||||
@Composable
|
||||
override fun present(): HomeSpacesState {
|
||||
val hideInvitesAvatar by remember {
|
||||
client
|
||||
.mediaPreviewService()
|
||||
.mediaPreviewConfigFlow
|
||||
.mapState { config -> config.hideInviteAvatar }
|
||||
}.collectAsState()
|
||||
val spaceRooms by client.spaceService.spaceRooms.collectAsState(emptyList())
|
||||
val seenSpaceInvites by remember {
|
||||
seenInvitesStore.seenSpaceIds().map { it.toPersistentSet() }
|
||||
}.collectAsState(persistentSetOf())
|
||||
|
||||
fun handleEvents(event: HomeSpacesEvents) {
|
||||
// when (event) { }
|
||||
}
|
||||
|
||||
return HomeSpacesState(
|
||||
space = CurrentSpace.Root,
|
||||
spaceRooms = spaceRooms,
|
||||
seenSpaceInvites = seenSpaceInvites,
|
||||
hideInvitesAvatar = hideInvitesAvatar,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.home.impl.spaces
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
data class HomeSpacesState(
|
||||
val space: CurrentSpace,
|
||||
val spaceRooms: List<SpaceRoom>,
|
||||
val seenSpaceInvites: ImmutableSet<SpaceId>,
|
||||
val hideInvitesAvatar: Boolean,
|
||||
val eventSink: (HomeSpacesEvents) -> Unit,
|
||||
)
|
||||
|
||||
sealed interface CurrentSpace {
|
||||
object Root : CurrentSpace
|
||||
data class Space(val spaceRoom: SpaceRoom) : CurrentSpace
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.home.impl.spaces
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
open class HomeSpacesStateProvider : PreviewParameterProvider<HomeSpacesState> {
|
||||
override val values: Sequence<HomeSpacesState>
|
||||
get() = sequenceOf(
|
||||
aHomeSpacesState(
|
||||
spaceRooms = SpaceRoomProvider().values.toList(),
|
||||
seenSpaceInvites = setOf(
|
||||
SpaceId("!spaceId3:example.com"),
|
||||
),
|
||||
),
|
||||
aHomeSpacesState(
|
||||
space = CurrentSpace.Space(
|
||||
spaceRoom = aSpaceRooms(spaceId = SpaceId("!mySpace:example.com"))
|
||||
),
|
||||
spaceRooms = aListOfSpaceRooms(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aHomeSpacesState(
|
||||
space: CurrentSpace = CurrentSpace.Root,
|
||||
spaceRooms: List<SpaceRoom> = aListOfSpaceRooms(),
|
||||
seenSpaceInvites: Set<SpaceId> = emptySet(),
|
||||
hideInvitesAvatar: Boolean = false,
|
||||
eventSink: (HomeSpacesEvents) -> Unit = {},
|
||||
) = HomeSpacesState(
|
||||
space = space,
|
||||
spaceRooms = spaceRooms,
|
||||
seenSpaceInvites = seenSpaceInvites.toImmutableSet(),
|
||||
hideInvitesAvatar = hideInvitesAvatar,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
fun aListOfSpaceRooms(): List<SpaceRoom> {
|
||||
return listOf(
|
||||
aSpaceRooms(spaceId = SpaceId("!spaceId0:example.com")),
|
||||
aSpaceRooms(spaceId = SpaceId("!spaceId1:example.com")),
|
||||
aSpaceRooms(spaceId = SpaceId("!spaceId2:example.com")),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.home.impl.spaces
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.ui.components.SpaceHeaderRootView
|
||||
import io.element.android.libraries.matrix.ui.components.SpaceHeaderView
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun HomeSpacesView(
|
||||
state: HomeSpacesState,
|
||||
onSpaceClick: (SpaceId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier) {
|
||||
val space = state.space
|
||||
when (space) {
|
||||
CurrentSpace.Root -> {
|
||||
item {
|
||||
SpaceHeaderRootView(
|
||||
numberOfSpaces = state.spaceRooms.size,
|
||||
// TODO
|
||||
numberOfRooms = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
is CurrentSpace.Space -> item {
|
||||
SpaceHeaderView(
|
||||
avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader),
|
||||
name = space.spaceRoom.name,
|
||||
topic = space.spaceRoom.topic,
|
||||
joinRule = space.spaceRoom.joinRule,
|
||||
heroes = space.spaceRoom.heroes.toImmutableList(),
|
||||
numberOfMembers = space.spaceRoom.numJoinedMembers,
|
||||
numberOfRooms = space.spaceRoom.childrenCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
state.spaceRooms.forEach {
|
||||
item(it.spaceId) {
|
||||
val isInvitation = it.state == CurrentUserMembership.INVITED
|
||||
HomeSpaceItemView(
|
||||
spaceRoom = it,
|
||||
showUnreadIndicator = isInvitation && it.spaceId !in state.seenSpaceInvites,
|
||||
hideAvatars = isInvitation && state.hideInvitesAvatar,
|
||||
onClick = {
|
||||
onSpaceClick(it.spaceId)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun HomeSpacesViewPreview(
|
||||
@PreviewParameter(HomeSpacesStateProvider::class) state: HomeSpacesState,
|
||||
) = ElementPreview {
|
||||
HomeSpacesView(
|
||||
state = state,
|
||||
onSpaceClick = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -12,6 +12,7 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.home.impl.roomlist.aRoomListState
|
||||
import io.element.android.features.home.impl.spaces.aHomeSpacesState
|
||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
|
|
@ -158,6 +159,7 @@ class HomePresenterTest {
|
|||
indicatorService = indicatorService,
|
||||
logoutPresenter = { aDirectLogoutState() },
|
||||
roomListPresenter = { aRoomListState() },
|
||||
homeSpacesPresenter = { aHomeSpacesState() },
|
||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.home.impl.spaces
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class HomeSpacesPresenterTest {
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
presenter.test {
|
||||
val state = awaitItem()
|
||||
assertThat(state.space).isEqualTo(CurrentSpace.Root)
|
||||
assertThat(state.spaceRooms).isEmpty()
|
||||
assertThat(state.hideInvitesAvatar).isFalse()
|
||||
assertThat(state.seenSpaceInvites).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
client: MatrixClient = FakeMatrixClient(),
|
||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
||||
) = HomeSpacesPresenter(
|
||||
client = client,
|
||||
seenInvitesStore = seenInvitesStore,
|
||||
)
|
||||
}
|
||||
|
|
@ -8,7 +8,10 @@
|
|||
package io.element.android.features.invite.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||
import io.element.android.libraries.matrix.api.core.toSpaceId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface SeenInvitesStore {
|
||||
/**
|
||||
|
|
@ -35,3 +38,9 @@ interface SeenInvitesStore {
|
|||
*/
|
||||
suspend fun clear()
|
||||
}
|
||||
|
||||
fun SeenInvitesStore.seenSpaceIds(): Flow<Set<SpaceId>> {
|
||||
return seenRoomIds().map { roomIds ->
|
||||
roomIds.map { it.toSpaceId() }.toSet()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -546,7 +546,7 @@ private fun DefaultLoadedContent(
|
|||
},
|
||||
memberCount = {
|
||||
if (contentState.showMemberCount) {
|
||||
MembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
|
||||
MembersCountMolecule(memberCount = contentState.numberOfMembers?.toInt() ?: 0)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
|
||||
@Composable
|
||||
fun MembersCountMolecule(
|
||||
memberCount: Long,
|
||||
memberCount: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<MatrixUser>
|
||||
val roomListService: RoomListService
|
||||
val spaceService: SpaceService
|
||||
val mediaLoader: MatrixMediaLoader
|
||||
val sessionCoroutineScope: CoroutineScope
|
||||
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
|
||||
|
|
|
|||
|
|
@ -20,3 +20,5 @@ value class RoomId(val value: String) : Serializable {
|
|||
|
||||
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.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,12 @@ class RustMatrixClient(
|
|||
roomSyncSubscriber = roomSyncSubscriber,
|
||||
)
|
||||
|
||||
override val spaceService: SpaceService = RustSpaceService(
|
||||
innerSpaceService = innerSpaceService,
|
||||
sessionCoroutineScope = sessionCoroutineScope,
|
||||
sessionDispatcher = sessionDispatcher,
|
||||
)
|
||||
|
||||
private val verificationService = RustSessionVerificationService(
|
||||
client = innerClient,
|
||||
isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running },
|
||||
|
|
@ -540,6 +549,7 @@ class RustMatrixClient(
|
|||
|
||||
sessionDelegate.clearCurrentClient()
|
||||
innerRoomListService.close()
|
||||
innerSpaceService.close()
|
||||
notificationService.close()
|
||||
encryptionService.close()
|
||||
innerClient.close()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* 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(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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import org.matrix.rustcomponents.sdk.PusherKind
|
|||
import org.matrix.rustcomponents.sdk.RoomDirectorySearch
|
||||
import org.matrix.rustcomponents.sdk.Session
|
||||
import org.matrix.rustcomponents.sdk.SessionVerificationController
|
||||
import org.matrix.rustcomponents.sdk.SpaceService
|
||||
import org.matrix.rustcomponents.sdk.SyncService
|
||||
import org.matrix.rustcomponents.sdk.SyncServiceBuilder
|
||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||
|
|
@ -52,6 +53,7 @@ class FakeFfiClient(
|
|||
override suspend fun cachedAvatarUrl(): String? = null
|
||||
override suspend fun restoreSession(session: Session) = Unit
|
||||
override fun syncService(): SyncServiceBuilder = FakeFfiSyncServiceBuilder()
|
||||
override fun spaceService(): SpaceService = FakeFfiSpaceService()
|
||||
override fun roomDirectorySearch(): RoomDirectorySearch = FakeFfiRoomDirectorySearch()
|
||||
override suspend fun setPusher(
|
||||
identifiers: PusherIdentifiers,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.fixtures.fakes
|
||||
|
||||
import org.matrix.rustcomponents.sdk.NoPointer
|
||||
import org.matrix.rustcomponents.sdk.SpaceService
|
||||
|
||||
class FakeFfiSpaceService : SpaceService(NoPointer)
|
||||
|
|
@ -30,6 +30,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.user.MatrixSearchUserResults
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
|
@ -42,6 +43,7 @@ import io.element.android.libraries.matrix.test.notificationsettings.FakeNotific
|
|||
import io.element.android.libraries.matrix.test.pushers.FakePushersService
|
||||
import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryService
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.libraries.matrix.test.spaces.FakeSpaceService
|
||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
|
@ -65,6 +67,7 @@ class FakeMatrixClient(
|
|||
private val userDisplayName: String? = A_USER_NAME,
|
||||
private val userAvatarUrl: String? = AN_AVATAR_URL,
|
||||
override val roomListService: RoomListService = FakeRoomListService(),
|
||||
override val spaceService: SpaceService = FakeSpaceService(),
|
||||
override val mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(),
|
||||
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
||||
private val pushersService: FakePushersService = FakePushersService(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.test.spaces
|
||||
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
class FakeSpaceService(
|
||||
private val joinedSpacesResult: () -> Result<List<SpaceRoom>> = { lambdaError() }
|
||||
) : SpaceService {
|
||||
private val _spaceRooms = MutableSharedFlow<List<SpaceRoom>>()
|
||||
override val spaceRooms: SharedFlow<List<SpaceRoom>>
|
||||
get() = _spaceRooms.asSharedFlow()
|
||||
|
||||
suspend fun emitSpaceRoomList(value: List<SpaceRoom>) {
|
||||
_spaceRooms.emit(value)
|
||||
}
|
||||
|
||||
override suspend fun joinedSpaces(): Result<List<SpaceRoom>> = simulateLongTask {
|
||||
return joinedSpacesResult()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=3643-2048
|
||||
*/
|
||||
@Composable
|
||||
fun SpaceHeaderRootView(
|
||||
numberOfSpaces: Int,
|
||||
numberOfRooms: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 32.dp, bottom = 24.dp, start = 16.dp, end = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
BigIcon(
|
||||
style = BigIcon.Style.Default(CompoundIcons.WorkspaceSolid())
|
||||
)
|
||||
Text(
|
||||
text = stringResource(CommonStrings.screen_space_list_title),
|
||||
style = ElementTheme.typography.fontHeadingLgBold,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
SpaceInfoRow(
|
||||
leftText = numberOfSpaces(numberOfSpaces),
|
||||
rightText = numberOfRooms(numberOfRooms),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(CommonStrings.screen_space_list_description),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SpaceHeaderRootViewPreview() = ElementPreview {
|
||||
SpaceHeaderRootView(
|
||||
numberOfSpaces = 3,
|
||||
numberOfRooms = 10,
|
||||
)
|
||||
}
|
||||
|
|
@ -38,11 +38,11 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
@Composable
|
||||
fun SpaceHeaderView(
|
||||
avatarData: AvatarData,
|
||||
name: String,
|
||||
topic: String,
|
||||
joinRule: JoinRule,
|
||||
name: String?,
|
||||
topic: String?,
|
||||
joinRule: JoinRule?,
|
||||
heroes: ImmutableList<MatrixUser>,
|
||||
numberOfMembers: Long,
|
||||
numberOfMembers: Int,
|
||||
numberOfRooms: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
topicMaxLines: Int = Int.MAX_VALUE,
|
||||
|
|
@ -58,29 +58,35 @@ fun SpaceHeaderView(
|
|||
avatarData = avatarData,
|
||||
avatarType = AvatarType.Space(false),
|
||||
)
|
||||
Text(
|
||||
text = name,
|
||||
style = ElementTheme.typography.fontHeadingLgBold,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
SpaceInfoRow(
|
||||
joinRule = joinRule,
|
||||
numberOfRooms = numberOfRooms,
|
||||
)
|
||||
name?.let {
|
||||
Text(
|
||||
text = name,
|
||||
style = ElementTheme.typography.fontHeadingLgBold,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
if (joinRule != null) {
|
||||
SpaceInfoRow(
|
||||
joinRule = joinRule,
|
||||
numberOfRooms = numberOfRooms,
|
||||
)
|
||||
}
|
||||
SpaceMembersView(
|
||||
heroes = heroes,
|
||||
numberOfMembers = numberOfMembers,
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
)
|
||||
Text(
|
||||
text = topic,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = topicMaxLines,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
topic?.let {
|
||||
Text(
|
||||
text = topic,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = topicMaxLines,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
@Composable
|
||||
fun SpaceMembersView(
|
||||
heroes: ImmutableList<MatrixUser>,
|
||||
numberOfMembers: Long,
|
||||
numberOfMembers: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (heroes.isEmpty()) {
|
||||
|
|
@ -60,7 +60,7 @@ fun SpaceMembersView(
|
|||
@Composable
|
||||
private fun SpaceMembersWithAvatar(
|
||||
heroes: ImmutableList<AvatarData>,
|
||||
numberOfMembers: Long,
|
||||
numberOfMembers: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a241e70cffe61158784ac75425e13cd8d2695406f6e52a93ca7dddfdc9f99caa
|
||||
size 14164
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:864ab3319f7074643b58d2dd6da22a4c5493bae25502c89bf2c3503bc57cfe91
|
||||
size 11628
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ba93e44b8732d7bf78b25200a5db6e8a329f970aa1fb6a002033c9ebfd09b805
|
||||
size 11723
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:59b6f98dc65ae4be92b7cdfde9778b390b6474205f75d48061d6345eece53675
|
||||
size 19034
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b4a8cef8dc61dd71458d7518548e83f5e4484fd9dc740fefc690769ffbf6bd80
|
||||
size 18949
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7ce65c4dd01458194792d0a14eafc66e6e6f0a0e0f59259e4661322d0cf9a46d
|
||||
size 13819
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3e3765e543cce879b97a35b8c3e1cdef4b6c9ce4b1739b9d9a544d6a56118399
|
||||
size 11414
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:51b283fb8e976c8717abeef61dcf294f3efd52b7291fdf023c09759e6df6513d
|
||||
size 11337
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c440b9628b8105b73f5f576c1b99269a74a89ab07f56080ad53cc888aca24da3
|
||||
size 18111
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9ddecc15d26dbdd648ec7abbff51718da03326ebf32a2d675563110c096a198b
|
||||
size 18141
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dff393d500d855842d84d311b3bb5cb60bab6fc2e8e9efe3b593d675218d5024
|
||||
size 123012
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:04ec1bd0bd667d5319843f480f2b7ca4b1b4364db4fbe22d85c10d291891642a
|
||||
size 42696
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9993c1937c780b4b7e3a47bfb53fa5735ac7bc20f29559281384ca32eb7c13ae
|
||||
size 120701
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0d00883e68471491d8edf06eee394e4310707dbd03782f57bb7f783822790b61
|
||||
size 41372
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b819420fc0df9dfd4348dcd95bf46652422307b55bc58283a27130b810f57ce5
|
||||
size 25671
|
||||
oid sha256:cd023c99c75dba5987f86951d46e821d378440a35335dc46894ff127951ee50f
|
||||
size 59135
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f38f42eae4af268bed21fa69b9426708c7544f0ff558bb7bbeda721d5da29755
|
||||
size 22931
|
||||
oid sha256:6b9f321dfd59e2a388c0d864c73eca01c29b7f96ee0481597ee3cd8ff1df1781
|
||||
size 56262
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6a4d295ae71ba2709845312b84983b79348069ded46b30091b9fa769a587cbb7
|
||||
size 14337
|
||||
oid sha256:26b9908bbd388321444037dcc1aae55037d3dabc7a9f9b14c39ba871f4f9d593
|
||||
size 16128
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:da8f54591a52475c6f47fa083a1bb397e1164dd3e735562388fde4ff45644150
|
||||
size 16275
|
||||
oid sha256:1a96b6d95b8941d80035b309444b9eaa038098fb16aa84dec209fc3ee215ac9e
|
||||
size 21687
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ce1aba18ea8a6b45d9c40de9af961952e261c7c6200ab13cdfa67906d995208b
|
||||
size 14888
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6a4d295ae71ba2709845312b84983b79348069ded46b30091b9fa769a587cbb7
|
||||
size 14337
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:da8f54591a52475c6f47fa083a1bb397e1164dd3e735562388fde4ff45644150
|
||||
size 16275
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:526485d4efe3b100a450885ab3620f180e587cd9fbe6e8a373032db14483a90b
|
||||
size 19284
|
||||
oid sha256:98a3562697e9d2d7f60a61976a8261b934dcfb75d195d9cf783f6cce4171dfc1
|
||||
size 16643
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f52dd1f56c484022be9b2d33c7647f2b4942096fc41928c8021dbb4b60588f81
|
||||
size 18429
|
||||
oid sha256:49ca1d5ef80f7572378ef40e7d73b1b669bcac515b3ef54042a7b67f9c3280d7
|
||||
size 15403
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3a1e82c60b70e7755a1e514d69c0da3c7a3ef93e8528f303e9478eb880c2b51f
|
||||
size 21531
|
||||
oid sha256:bc5b79a8448cbbd9dd73dc416c8b6d051e335845da2caedcdb9fdc3414c4f9c6
|
||||
size 20024
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a3e85c12d1152986e307ae40753453f5caf789bbfdc00bbc084dbe08f744f4e9
|
||||
size 17424
|
||||
oid sha256:526485d4efe3b100a450885ab3620f180e587cd9fbe6e8a373032db14483a90b
|
||||
size 19284
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c2c7116dcf6b9ef6a7281b9dccfdfa92589869e897d7d9781c78c3f422d75533
|
||||
size 16118
|
||||
oid sha256:f52dd1f56c484022be9b2d33c7647f2b4942096fc41928c8021dbb4b60588f81
|
||||
size 18429
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2cb816c3e37b3a8c1e0d31a2d7ec05344e7bd16ccd214bb15475754013f0927d
|
||||
size 21038
|
||||
oid sha256:3a1e82c60b70e7755a1e514d69c0da3c7a3ef93e8528f303e9478eb880c2b51f
|
||||
size 21531
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:33e206508e839a7517f0621a79571e52c61b225f57efab0379a9bdb510634351
|
||||
size 19053
|
||||
oid sha256:a3e85c12d1152986e307ae40753453f5caf789bbfdc00bbc084dbe08f744f4e9
|
||||
size 17424
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d896fcf527b98ae449f52bdaa77ef5a230756e3723681afdc549f7aa1ba11c42
|
||||
size 16867
|
||||
oid sha256:c2c7116dcf6b9ef6a7281b9dccfdfa92589869e897d7d9781c78c3f422d75533
|
||||
size 16118
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed5b27869ebf46171cbedb8c57d0163965d77d362445d1edd2ae307371c756ab
|
||||
size 24687
|
||||
oid sha256:2cb816c3e37b3a8c1e0d31a2d7ec05344e7bd16ccd214bb15475754013f0927d
|
||||
size 21038
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2ef22b332b3383732def98b137c6986afcc3414e5e7bcf5b568956bd8f1b5c8c
|
||||
size 14443
|
||||
oid sha256:33e206508e839a7517f0621a79571e52c61b225f57efab0379a9bdb510634351
|
||||
size 19053
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2ef70634dbed00e908fb43116fcb8d6e2c7084f90f36cca32b83498cf07e1f6a
|
||||
size 13606
|
||||
oid sha256:d896fcf527b98ae449f52bdaa77ef5a230756e3723681afdc549f7aa1ba11c42
|
||||
size 16867
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aaa1497201d95eb9b39dba91326c179f0d2e1a58ea70772067d46e0e7dcc0d0c
|
||||
size 16677
|
||||
oid sha256:ed5b27869ebf46171cbedb8c57d0163965d77d362445d1edd2ae307371c756ab
|
||||
size 24687
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f05acb090eb967a126bb1c4741f763bc7ba254e2d32641d7f6b7d0c3888f5f44
|
||||
size 16522
|
||||
oid sha256:2ef22b332b3383732def98b137c6986afcc3414e5e7bcf5b568956bd8f1b5c8c
|
||||
size 14443
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:408498059aa91ae83ec3617172dd1af1ace78c99b172c3da1895d125965a4bd6
|
||||
size 15272
|
||||
oid sha256:2ef70634dbed00e908fb43116fcb8d6e2c7084f90f36cca32b83498cf07e1f6a
|
||||
size 13606
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:884eb2d69af3e1d91fddcbcd7173ac547530e5f3439ad2edc3df6cdbbc5ac312
|
||||
size 19914
|
||||
oid sha256:aaa1497201d95eb9b39dba91326c179f0d2e1a58ea70772067d46e0e7dcc0d0c
|
||||
size 16677
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:35db9f05aa3c3226c611cd83b9f6f9dd72b70c9b6bb4f68ebb8e0c596b30c99a
|
||||
size 17591
|
||||
oid sha256:f05acb090eb967a126bb1c4741f763bc7ba254e2d32641d7f6b7d0c3888f5f44
|
||||
size 16522
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:74de00644b39d999afc93936939e3ff2066f5b9b526d59bad20c50e318ee765e
|
||||
size 16280
|
||||
oid sha256:408498059aa91ae83ec3617172dd1af1ace78c99b172c3da1895d125965a4bd6
|
||||
size 15272
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2e4ac9b255de3602f03a854b8ba71703176253a5c2eeacb364a6c87e89e8f193
|
||||
size 21200
|
||||
oid sha256:884eb2d69af3e1d91fddcbcd7173ac547530e5f3439ad2edc3df6cdbbc5ac312
|
||||
size 19914
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2c4335667e4c604526427826e5d56a09dad85a423844f974f55e6ebda17d42a0
|
||||
size 16617
|
||||
oid sha256:35db9f05aa3c3226c611cd83b9f6f9dd72b70c9b6bb4f68ebb8e0c596b30c99a
|
||||
size 17591
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:27df1354079b41fbed84791777d9e54198c3576418f6d5027b4b88ff22454584
|
||||
size 14821
|
||||
oid sha256:74de00644b39d999afc93936939e3ff2066f5b9b526d59bad20c50e318ee765e
|
||||
size 16280
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ad1df854d799ccb28358aa5a7c61a7ec4627b6b766813c9d3a3a6b7d423c5a1a
|
||||
size 21401
|
||||
oid sha256:2e4ac9b255de3602f03a854b8ba71703176253a5c2eeacb364a6c87e89e8f193
|
||||
size 21200
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2d07f11e284078387bf9ab84e6b79fd35371486ba79fca7ccdfde6e316d7549f
|
||||
size 14371
|
||||
oid sha256:2c4335667e4c604526427826e5d56a09dad85a423844f974f55e6ebda17d42a0
|
||||
size 16617
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:05090ff82f6f6fe27e9a2986b47c4aae72c28c0335532534d9a4b65b00613d24
|
||||
size 13634
|
||||
oid sha256:27df1354079b41fbed84791777d9e54198c3576418f6d5027b4b88ff22454584
|
||||
size 14821
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:732c17b7a00cfde8e52aaefe9b02bc009ec840d2cf72adbbd9bbab010cc2b12b
|
||||
size 16295
|
||||
oid sha256:ad1df854d799ccb28358aa5a7c61a7ec4627b6b766813c9d3a3a6b7d423c5a1a
|
||||
size 21401
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:005c5f7cf69f2452d6fe894e02ae030aa2d6692181b0e94552b33064e755fbec
|
||||
size 14999
|
||||
oid sha256:2d07f11e284078387bf9ab84e6b79fd35371486ba79fca7ccdfde6e316d7549f
|
||||
size 14371
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e9c435ebe2e1c7b729c96e75ffa57e488b157a0e7632be7123cb0f4542770a1d
|
||||
size 14259
|
||||
oid sha256:05090ff82f6f6fe27e9a2986b47c4aae72c28c0335532534d9a4b65b00613d24
|
||||
size 13634
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:00680313760f229b844a3109530e9c0691b3b02917b3f5e18ac0c8b07b09724c
|
||||
size 16914
|
||||
oid sha256:732c17b7a00cfde8e52aaefe9b02bc009ec840d2cf72adbbd9bbab010cc2b12b
|
||||
size 16295
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:989b61bfa802cbf66168c6484366b85d0f683416c0b8ad6c2c59b7a45ed19313
|
||||
size 15436
|
||||
oid sha256:005c5f7cf69f2452d6fe894e02ae030aa2d6692181b0e94552b33064e755fbec
|
||||
size 14999
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fe2ac65f4be2460a563a07cc64e7b592efb850c9a8d72c26adf6d326bdd200d9
|
||||
size 15094
|
||||
oid sha256:e9c435ebe2e1c7b729c96e75ffa57e488b157a0e7632be7123cb0f4542770a1d
|
||||
size 14259
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:33b2f35b4be757434e86bce5062551a5ef5af4760016c1751b984e333342841f
|
||||
size 16373
|
||||
oid sha256:00680313760f229b844a3109530e9c0691b3b02917b3f5e18ac0c8b07b09724c
|
||||
size 16914
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fd18a15bc49e87b8abc9231a13771a0fe34003b11fe1fd48df2d91f54bc40cc8
|
||||
size 15564
|
||||
oid sha256:989b61bfa802cbf66168c6484366b85d0f683416c0b8ad6c2c59b7a45ed19313
|
||||
size 15436
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:388a1ac9f1790fd305fd329b73ae621093d323db3fa4d4d32eef92f8dd5b51ec
|
||||
size 14824
|
||||
oid sha256:fe2ac65f4be2460a563a07cc64e7b592efb850c9a8d72c26adf6d326bdd200d9
|
||||
size 15094
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bc90e78d2fa94425322895028f1156bfda196ee2a761231920c6ffd80f02984b
|
||||
size 17512
|
||||
oid sha256:33b2f35b4be757434e86bce5062551a5ef5af4760016c1751b984e333342841f
|
||||
size 16373
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:74fda6437495995876f76dc1ff0e056de4937ef8d719b3be1ce73baccd31516e
|
||||
size 15889
|
||||
oid sha256:fd18a15bc49e87b8abc9231a13771a0fe34003b11fe1fd48df2d91f54bc40cc8
|
||||
size 15564
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:04fb2f230a09f0d3e27e151d98aa5d91e416b5ca9637b4db5d8e00118a68e7c4
|
||||
size 15166
|
||||
oid sha256:388a1ac9f1790fd305fd329b73ae621093d323db3fa4d4d32eef92f8dd5b51ec
|
||||
size 14824
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:896f698ef11d8a0577baf0a81cb54cabc307d95d05c230e5e3bdde40c3dc0900
|
||||
size 17844
|
||||
oid sha256:bc90e78d2fa94425322895028f1156bfda196ee2a761231920c6ffd80f02984b
|
||||
size 17512
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cf264b6f8af5b7432511d595ff8c663c5e2be33c9f85268627f5188e3f0f8db0
|
||||
size 18949
|
||||
oid sha256:74fda6437495995876f76dc1ff0e056de4937ef8d719b3be1ce73baccd31516e
|
||||
size 15889
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0bbe1e3e8e1ea119cc61509617d3fa8e2bd047f619c41271901c73c46be1d610
|
||||
size 18201
|
||||
oid sha256:04fb2f230a09f0d3e27e151d98aa5d91e416b5ca9637b4db5d8e00118a68e7c4
|
||||
size 15166
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7a7c183808801645e285dfba563c036c204a347de20b4b1e40fcfeab29fafb7d
|
||||
size 20876
|
||||
oid sha256:896f698ef11d8a0577baf0a81cb54cabc307d95d05c230e5e3bdde40c3dc0900
|
||||
size 17844
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a48eb99f4466e3a883c0c7441006f201e0db22fd0e920ad0a77a8512637e01bb
|
||||
size 16445
|
||||
oid sha256:cf264b6f8af5b7432511d595ff8c663c5e2be33c9f85268627f5188e3f0f8db0
|
||||
size 18949
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:895ba9a35b8854d5005fc0671a7e6cbba5e26525b4a8ed4fa0fb612432caa04e
|
||||
size 15205
|
||||
oid sha256:0bbe1e3e8e1ea119cc61509617d3fa8e2bd047f619c41271901c73c46be1d610
|
||||
size 18201
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:98baed819cd8b08085ceabf7dbccccc77b0fdf0d28a3f852879f6f8aa02ee441
|
||||
size 19848
|
||||
oid sha256:7a7c183808801645e285dfba563c036c204a347de20b4b1e40fcfeab29fafb7d
|
||||
size 20876
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c4678c15b608547c255d5eeb128c144ae7a3c5a21de2b047fc77f15364822ac2
|
||||
size 12847
|
||||
oid sha256:a48eb99f4466e3a883c0c7441006f201e0db22fd0e920ad0a77a8512637e01bb
|
||||
size 16445
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e30395594c1a35d90fc0a04af340eb79cbe686d90856414d0024b769f86e89d
|
||||
size 12507
|
||||
oid sha256:895ba9a35b8854d5005fc0671a7e6cbba5e26525b4a8ed4fa0fb612432caa04e
|
||||
size 15205
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue