Merge pull request #6150 from element-hq/feature/fga/space_ui_tweaks

Iterate on Space related UI
This commit is contained in:
ganfra 2026-02-10 11:36:09 +01:00 committed by GitHub
commit ee0da6b15c
176 changed files with 695 additions and 583 deletions

View file

@ -82,10 +82,6 @@ class ConfigureRoomPresenter(
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA) private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
private var pendingPermissionRequest = false private var pendingPermissionRequest = false
init {
dataStore.setIsSpace(isSpace)
}
@Composable @Composable
override fun present(): ConfigureRoomState { override fun present(): ConfigureRoomState {
val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false) val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
@ -123,9 +119,10 @@ class ConfigureRoomPresenter(
} else { } else {
persistentListOf() persistentListOf()
} }
val parentSpace = spaces.find { it.roomId == initialParentSpaceId } val parentSpace = spaces.find { it.roomId == initialParentSpaceId }
parentSpace?.let { dataStore.setParentSpace(it) } parentSpace?.let {
dataStore.setParentSpace(parentSpace = parentSpace, updateVisibility = true)
}
} }
LaunchedEffect(cameraPermissionState.permissionGranted) { LaunchedEffect(cameraPermissionState.permissionGranted) {
@ -152,21 +149,42 @@ class ConfigureRoomPresenter(
// 2. If it has a parent space. // 2. If it has a parent space.
// 3. If knocking is enabled. // 3. If knocking is enabled.
val parentSpace = createRoomConfig.parentSpace val parentSpace = createRoomConfig.parentSpace
val availableJoinRules = remember(createRoomConfig.parentSpace, isSpace, isKnockFeatureEnabled) { val availableJoinRules = remember(parentSpace, isSpace, isKnockFeatureEnabled) {
when { when {
isSpace && parentSpace != null -> TODO("Adding a space to a parent space is not supported yet! How did you get here?") isSpace && parentSpace != null -> TODO("Adding a space to a parent space is not supported yet! How did you get here?")
parentSpace == null || parentSpace.joinRule == JoinRule.Public -> listOfNotNull( parentSpace == null || parentSpace.joinRule == JoinRule.Public -> listOfNotNull(
JoinRuleItem.PublicVisibility.Public, JoinRuleItem.PublicVisibility.Public,
JoinRuleItem.PublicVisibility.AskToJoin.takeIf { !isSpace && isKnockFeatureEnabled }, JoinRuleItem.PublicVisibility.AskToJoin.takeIf { !isSpace && isKnockFeatureEnabled },
JoinRuleItem.Private, JoinRuleItem.PrivateVisibility.Private,
).toImmutableList() ).toImmutableList()
else -> listOfNotNull( else -> listOfNotNull(
JoinRuleItem.PublicVisibility.Restricted(parentSpace.roomId), JoinRuleItem.PrivateVisibility.Restricted(parentSpace.roomId),
JoinRuleItem.PublicVisibility.AskToJoinRestricted(parentSpace.roomId).takeIf { !isSpace && isKnockFeatureEnabled }, JoinRuleItem.PrivateVisibility.AskToJoinRestricted(parentSpace.roomId).takeIf { isKnockFeatureEnabled },
JoinRuleItem.Private, JoinRuleItem.PrivateVisibility.Private,
).toImmutableList() ).toImmutableList()
} }
} }
val currentJoinRule = createRoomConfig.visibilityState.joinRuleItem
LaunchedEffect(availableJoinRules, currentJoinRule) {
// Find matching rule by type (ignoring parentSpaceId parameter for Restricted types)
val matchingRule = when (currentJoinRule) {
is JoinRuleItem.PrivateVisibility.Restricted ->
availableJoinRules.filterIsInstance<JoinRuleItem.PrivateVisibility.Restricted>().firstOrNull()
is JoinRuleItem.PrivateVisibility.AskToJoinRestricted ->
availableJoinRules.filterIsInstance<JoinRuleItem.PrivateVisibility.AskToJoinRestricted>().firstOrNull()
else -> availableJoinRules.find { it == currentJoinRule }
}
when {
matchingRule == null -> {
// No matching type fallback to Private (always available)
dataStore.setJoinRule(JoinRuleItem.PrivateVisibility.Private)
}
matchingRule != currentJoinRule -> {
// Same type but different params (e.g., different parentSpaceId), update
dataStore.setJoinRule(matchingRule)
}
}
}
fun createRoom(config: CreateRoomConfig) { fun createRoom(config: CreateRoomConfig) {
createRoomAction.value = AsyncAction.Uninitialized createRoomAction.value = AsyncAction.Uninitialized
@ -193,7 +211,7 @@ class ConfigureRoomPresenter(
} }
} }
is ConfigureRoomEvents.SetParentSpace -> { is ConfigureRoomEvents.SetParentSpace -> {
dataStore.setParentSpace(event.space) dataStore.setParentSpace(event.space, false)
} }
ConfigureRoomEvents.CancelCreateRoom -> { ConfigureRoomEvents.CancelCreateRoom -> {
createRoomAction.value = AsyncAction.Uninitialized createRoomAction.value = AsyncAction.Uninitialized
@ -210,6 +228,7 @@ class ConfigureRoomPresenter(
roomAddressValidity = roomAddressValidity.value, roomAddressValidity = roomAddressValidity.value,
availableJoinRules = availableJoinRules, availableJoinRules = availableJoinRules,
spaces = spaces, spaces = spaces,
isSpace = isSpace,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
@ -220,35 +239,41 @@ class ConfigureRoomPresenter(
) = launch { ) = launch {
suspend { suspend {
val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) } val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) }
val params = if (config.visibilityState is RoomVisibilityState.Public) { val params = when (config.visibilityState) {
CreateRoomParameters( is RoomVisibilityState.Public -> {
name = config.roomName, CreateRoomParameters(
topic = config.topic, name = config.roomName,
isEncrypted = false, topic = config.topic,
isDirect = false, isEncrypted = false,
visibility = RoomVisibility.Public, isDirect = false,
joinRuleOverride = config.visibilityState.joinRuleItem.toJoinRule() visibility = RoomVisibility.Public,
// No need to specify the public join rule override, since the preset is already PUBLIC_CHAT joinRuleOverride = config.visibilityState.joinRuleItem.toJoinRule()
.takeIf { it != JoinRule.Public }, // No need to specify the public join rule override, since the preset is already PUBLIC_CHAT
preset = RoomPreset.PUBLIC_CHAT, .takeIf { it != JoinRule.Public },
invite = config.invites.map { it.userId }, preset = RoomPreset.PUBLIC_CHAT,
avatar = avatarUrl, invite = config.invites.map { it.userId },
roomAliasName = config.visibilityState.roomAddress(), avatar = avatarUrl,
isSpace = isSpace, roomAliasName = config.visibilityState.roomAddress(),
) isSpace = isSpace,
} else { )
CreateRoomParameters( }
name = config.roomName, is RoomVisibilityState.Private -> {
topic = config.topic, CreateRoomParameters(
isEncrypted = config.visibilityState is RoomVisibilityState.Private, name = config.roomName,
isDirect = false, topic = config.topic,
visibility = RoomVisibility.Private, isEncrypted = true,
historyVisibilityOverride = RoomHistoryVisibility.Invited, isDirect = false,
preset = RoomPreset.PRIVATE_CHAT, visibility = RoomVisibility.Private,
invite = config.invites.map { it.userId }, historyVisibilityOverride = RoomHistoryVisibility.Invited,
avatar = avatarUrl, joinRuleOverride = config.visibilityState.joinRuleItem.toJoinRule()
isSpace = isSpace, // No need to specify the Invite join rule override, since the preset is already PRIVATE_CHAT
) .takeIf { it != JoinRule.Invite },
preset = RoomPreset.PRIVATE_CHAT,
invite = config.invites.map { it.userId },
avatar = avatarUrl,
isSpace = isSpace,
)
}
} }
val roomId = matrixClient.createRoom(params) val roomId = matrixClient.createRoom(params)
.onFailure { failure -> .onFailure { failure ->

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.permissions.api.PermissionsState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class ConfigureRoomState( data class ConfigureRoomState(
val isSpace: Boolean,
val config: CreateRoomConfig, val config: CreateRoomConfig,
val avatarActions: ImmutableList<AvatarAction>, val avatarActions: ImmutableList<AvatarAction>,
val createRoomAction: AsyncAction<RoomId>, val createRoomAction: AsyncAction<RoomId>,
@ -28,5 +29,6 @@ data class ConfigureRoomState(
val eventSink: (ConfigureRoomEvents) -> Unit val eventSink: (ConfigureRoomEvents) -> Unit
) { ) {
val isValid: Boolean = config.roomName?.isNotEmpty() == true && val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
(config.visibilityState is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid) (config.visibilityState is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid) &&
config.visibilityState.joinRuleItem in availableJoinRules
} }

View file

@ -82,8 +82,8 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
roomAddressValidity = RoomAddressValidity.Valid, roomAddressValidity = RoomAddressValidity.Valid,
), ),
aConfigureRoomState( aConfigureRoomState(
isSpace = true,
config = CreateRoomConfig( config = CreateRoomConfig(
isSpace = true,
roomName = "Space 101", roomName = "Space 101",
topic = "Space topic for this space when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines", topic = "Space topic for this space when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
visibilityState = RoomVisibilityState.Public( visibilityState = RoomVisibilityState.Public(
@ -95,13 +95,11 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
), ),
aConfigureRoomState( aConfigureRoomState(
config = CreateRoomConfig( config = CreateRoomConfig(
isSpace = false,
roomName = "Room 101", roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines", topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
parentSpace = null, parentSpace = null,
visibilityState = RoomVisibilityState.Public( visibilityState = RoomVisibilityState.Private(
roomAddress = RoomAddress.AutoFilled("Space-101"), joinRuleItem = JoinRuleItem.PrivateVisibility.Restricted(aSpaceRoom().roomId),
joinRuleItem = JoinRuleItem.PublicVisibility.Restricted(aSpaceRoom().roomId),
), ),
), ),
spaces = listOf(aSpaceRoom()), spaces = listOf(aSpaceRoom()),
@ -109,13 +107,11 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
), ),
aConfigureRoomState( aConfigureRoomState(
config = CreateRoomConfig( config = CreateRoomConfig(
isSpace = false,
roomName = "Room 101", roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines", topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
parentSpace = aSpaceRoom(canonicalAlias = RoomAlias("#a-space-room:example.org")), parentSpace = aSpaceRoom(canonicalAlias = RoomAlias("#a-space-room:example.org")),
visibilityState = RoomVisibilityState.Public( visibilityState = RoomVisibilityState.Private(
roomAddress = RoomAddress.AutoFilled("Space-101"), joinRuleItem = JoinRuleItem.PrivateVisibility.Restricted(aSpaceRoom().roomId),
joinRuleItem = JoinRuleItem.PublicVisibility.Restricted(aSpaceRoom().roomId),
), ),
), ),
spaces = listOf(aSpaceRoom()), spaces = listOf(aSpaceRoom()),
@ -126,6 +122,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
fun aConfigureRoomState( fun aConfigureRoomState(
config: CreateRoomConfig = CreateRoomConfig(), config: CreateRoomConfig = CreateRoomConfig(),
isSpace: Boolean = false,
isKnockFeatureEnabled: Boolean = true, isKnockFeatureEnabled: Boolean = true,
avatarActions: List<AvatarAction> = emptyList(), avatarActions: List<AvatarAction> = emptyList(),
createRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized, createRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
@ -134,21 +131,22 @@ fun aConfigureRoomState(
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid, roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
availableVisibilityOptions: List<JoinRuleItem> = if (config.parentSpace != null) { availableVisibilityOptions: List<JoinRuleItem> = if (config.parentSpace != null) {
listOfNotNull( listOfNotNull(
JoinRuleItem.PublicVisibility.Restricted(config.parentSpace.roomId), JoinRuleItem.PrivateVisibility.Restricted(config.parentSpace.roomId),
JoinRuleItem.PublicVisibility.AskToJoinRestricted(config.parentSpace.roomId).takeIf { isKnockFeatureEnabled }, JoinRuleItem.PrivateVisibility.AskToJoinRestricted(config.parentSpace.roomId).takeIf { isKnockFeatureEnabled },
JoinRuleItem.Private, JoinRuleItem.PrivateVisibility.Private,
) )
} else { } else {
listOfNotNull( listOfNotNull(
JoinRuleItem.PublicVisibility.Public, JoinRuleItem.PublicVisibility.Public,
JoinRuleItem.PublicVisibility.AskToJoin.takeIf { isKnockFeatureEnabled }, JoinRuleItem.PublicVisibility.AskToJoin.takeIf { isKnockFeatureEnabled },
JoinRuleItem.Private, JoinRuleItem.PrivateVisibility.Private,
) )
}, },
spaces: List<SpaceRoom> = emptyList(), spaces: List<SpaceRoom> = emptyList(),
eventSink: (ConfigureRoomEvents) -> Unit = { }, eventSink: (ConfigureRoomEvents) -> Unit = { },
) = ConfigureRoomState( ) = ConfigureRoomState(
config = config, config = config,
isSpace = isSpace,
avatarActions = avatarActions.toImmutableList(), avatarActions = avatarActions.toImmutableList(),
createRoomAction = createRoomAction, createRoomAction = createRoomAction,
cameraPermissionState = cameraPermissionState, cameraPermissionState = cameraPermissionState,

View file

@ -14,7 +14,9 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -76,7 +78,7 @@ fun ConfigureRoomView(
onCreateRoomSuccess: (RoomId) -> Unit, onCreateRoomSuccess: (RoomId) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val isSpace = state.config.isSpace val isSpace = state.isSpace
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val isAvatarActionsSheetVisible = remember { mutableStateOf(false) } val isAvatarActionsSheetVisible = remember { mutableStateOf(false) }
@ -105,7 +107,6 @@ fun ConfigureRoomView(
.imePadding() .imePadding()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.consumeWindowInsets(padding), .consumeWindowInsets(padding),
verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
RoomNameWithAvatar( RoomNameWithAvatar(
isSpace = isSpace, isSpace = isSpace,
@ -115,20 +116,20 @@ fun ConfigureRoomView(
onAvatarClick = ::onAvatarClick, onAvatarClick = ::onAvatarClick,
onChangeRoomName = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) }, onChangeRoomName = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
) )
Spacer(modifier = Modifier.height(16.dp))
RoomTopic( RoomTopic(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
topic = state.config.topic.orEmpty(), topic = state.config.topic.orEmpty(),
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) }, onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
) )
Spacer(modifier = Modifier.height(16.dp))
if (!state.config.isSpace && state.spaces.isNotEmpty()) { if (!state.isSpace && state.spaces.isNotEmpty()) {
SelectParentSpaceOptions( SelectParentSpaceOptions(
spaces = state.spaces, spaces = state.spaces,
selectedSpace = state.config.parentSpace, selectedSpace = state.config.parentSpace,
onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) }, onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) },
) )
} }
RoomJoinRuleOptions( RoomJoinRuleOptions(
options = state.availableJoinRules, options = state.availableJoinRules,
selected = state.config.visibilityState.joinRuleItem, selected = state.config.visibilityState.joinRuleItem,
@ -138,20 +139,17 @@ fun ConfigureRoomView(
state.eventSink(ConfigureRoomEvents.JoinRuleChanged(it)) state.eventSink(ConfigureRoomEvents.JoinRuleChanged(it))
}, },
) )
if (state.config.visibilityState !is RoomVisibilityState.Private) { if (state.config.visibilityState !is RoomVisibilityState.Private) {
Column { ListSectionHeader(title = stringResource(R.string.screen_create_room_room_address_section_title))
ListSectionHeader(title = stringResource(R.string.screen_create_room_room_address_section_title)) RoomAddressField(
RoomAddressField( modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier.padding(horizontal = 16.dp), address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(), homeserverName = state.homeserverName,
homeserverName = state.homeserverName, addressValidity = state.roomAddressValidity,
addressValidity = state.roomAddressValidity, onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) }, label = null,
label = null, supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer), )
)
}
} }
} }
} }
@ -217,7 +215,9 @@ private fun RoomNameWithAvatar(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Box( Box(
modifier = Modifier.padding(end = 8.dp).size(AvatarSize.EditRoomDetails.dp), modifier = Modifier
.padding(end = 8.dp)
.size(AvatarSize.EditRoomDetails.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
val avatarState = remember(avatarUri) { val avatarState = remember(avatarUri) {
@ -272,12 +272,13 @@ private fun RoomTopic(
internal fun ConfigureRoomOptions( internal fun ConfigureRoomOptions(
title: String, title: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
hasDivider: Boolean = true,
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
Column( Column(
modifier = modifier.selectableGroup() modifier = modifier.selectableGroup()
) { ) {
ListSectionHeader(title = title) ListSectionHeader(title = title, hasDivider = hasDivider)
content() content()
} }
} }
@ -302,10 +303,10 @@ private fun RoomJoinRuleOptions(
size = RoundedIconAtomSize.Big, size = RoundedIconAtomSize.Big,
imageVector = when (item) { imageVector = when (item) {
JoinRuleItem.PublicVisibility.Public -> CompoundIcons.Public() JoinRuleItem.PublicVisibility.Public -> CompoundIcons.Public()
is JoinRuleItem.PublicVisibility.Restricted -> CompoundIcons.Space() is JoinRuleItem.PrivateVisibility.Restricted -> CompoundIcons.Space()
JoinRuleItem.PublicVisibility.AskToJoin, JoinRuleItem.PublicVisibility.AskToJoin,
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> CompoundIcons.UserAdd() is JoinRuleItem.PrivateVisibility.AskToJoinRestricted -> CompoundIcons.UserAdd()
JoinRuleItem.Private -> CompoundIcons.Lock() JoinRuleItem.PrivateVisibility.Private -> CompoundIcons.Lock()
}, },
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary, tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
backgroundTint = Color.Transparent, backgroundTint = Color.Transparent,
@ -314,28 +315,28 @@ private fun RoomJoinRuleOptions(
headlineContent = { headlineContent = {
val title = when (item) { val title = when (item) {
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_title) JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_title)
is JoinRuleItem.PublicVisibility.Restricted -> stringResource(R.string.screen_create_room_room_access_section_restricted_option_title) is JoinRuleItem.PrivateVisibility.Restricted -> stringResource(R.string.screen_create_room_room_access_section_restricted_option_title)
JoinRuleItem.PublicVisibility.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_title) JoinRuleItem.PublicVisibility.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_title)
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> stringResource( is JoinRuleItem.PrivateVisibility.AskToJoinRestricted -> stringResource(
R.string.screen_create_room_room_access_section_knocking_restricted_option_title R.string.screen_create_room_room_access_section_knocking_restricted_option_title
) )
JoinRuleItem.Private -> stringResource(R.string.screen_create_room_room_access_section_private_option_title) JoinRuleItem.PrivateVisibility.Private -> stringResource(R.string.screen_create_room_room_access_section_private_option_title)
} }
Text(text = title) Text(text = title)
}, },
supportingContent = { supportingContent = {
val description = when (item) { val description = when (item) {
JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_description) JoinRuleItem.PublicVisibility.Public -> stringResource(R.string.screen_create_room_room_access_section_public_option_description)
is JoinRuleItem.PublicVisibility.Restricted -> stringResource( is JoinRuleItem.PrivateVisibility.Restricted -> stringResource(
R.string.screen_create_room_room_access_section_restricted_option_description, R.string.screen_create_room_room_access_section_restricted_option_description,
parentSpace?.displayName.orEmpty() parentSpace?.displayName.orEmpty()
) )
JoinRuleItem.PublicVisibility.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_description) JoinRuleItem.PublicVisibility.AskToJoin -> stringResource(R.string.screen_create_room_room_access_section_knocking_option_description)
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> stringResource( is JoinRuleItem.PrivateVisibility.AskToJoinRestricted -> stringResource(
R.string.screen_create_room_room_access_section_knocking_restricted_option_description, R.string.screen_create_room_room_access_section_knocking_restricted_option_description,
parentSpace?.displayName.orEmpty() parentSpace?.displayName.orEmpty()
) )
JoinRuleItem.Private -> stringResource(R.string.screen_create_room_room_access_section_private_option_description) JoinRuleItem.PrivateVisibility.Private -> stringResource(R.string.screen_create_room_room_access_section_private_option_description)
} }
Text(text = description) Text(text = description)
}, },

View file

@ -14,11 +14,10 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
data class CreateRoomConfig( data class CreateRoomConfig(
val isSpace: Boolean = false,
val roomName: String? = null, val roomName: String? = null,
val topic: String? = null, val topic: String? = null,
val avatarUri: String? = null, val avatarUri: String? = null,
val invites: ImmutableList<MatrixUser> = persistentListOf(), val invites: ImmutableList<MatrixUser> = persistentListOf(),
val visibilityState: RoomVisibilityState = RoomVisibilityState.Private(), val visibilityState: RoomVisibilityState = RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private),
val parentSpace: SpaceRoom? = null, val parentSpace: SpaceRoom? = null,
) )

View file

@ -72,7 +72,9 @@ class CreateRoomConfigStore(
createRoomConfigFlow.getAndUpdate { config -> createRoomConfigFlow.getAndUpdate { config ->
config.copy( config.copy(
visibilityState = when (joinRule) { visibilityState = when (joinRule) {
JoinRuleItem.Private -> RoomVisibilityState.Private() is JoinRuleItem.PrivateVisibility -> RoomVisibilityState.Private(
joinRuleItem = joinRule
)
is JoinRuleItem.PublicVisibility -> { is JoinRuleItem.PublicVisibility -> {
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty()) val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
RoomVisibilityState.Public( RoomVisibilityState.Public(
@ -99,17 +101,16 @@ class CreateRoomConfigStore(
} }
} }
fun setIsSpace(isSpace: Boolean) { fun setParentSpace(parentSpace: SpaceRoom?, updateVisibility: Boolean) {
createRoomConfigFlow.getAndUpdate { config ->
config.copy(isSpace = isSpace)
}
}
fun setParentSpace(parentSpace: SpaceRoom?) {
createRoomConfigFlow.getAndUpdate { config -> createRoomConfigFlow.getAndUpdate { config ->
val visibilityState = if (parentSpace != null && updateVisibility) {
RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Restricted(parentSpace.roomId))
} else {
config.visibilityState
}
config.copy( config.copy(
parentSpace = parentSpace, parentSpace = parentSpace,
visibilityState = RoomVisibilityState.Private(), visibilityState = visibilityState
) )
} }
} }

View file

@ -18,7 +18,11 @@ import kotlinx.collections.immutable.persistentListOf
*/ */
@Immutable @Immutable
sealed interface JoinRuleItem { sealed interface JoinRuleItem {
data object Private : JoinRuleItem sealed interface PrivateVisibility : JoinRuleItem {
data object Private : PrivateVisibility
data class Restricted(val parentSpaceId: RoomId) : PrivateVisibility
data class AskToJoinRestricted(val parentSpaceId: RoomId) : PrivateVisibility
}
/** /**
* Those join rule items that represent public visibility of the room/space. * Those join rule items that represent public visibility of the room/space.
@ -27,18 +31,16 @@ sealed interface JoinRuleItem {
sealed interface PublicVisibility : JoinRuleItem { sealed interface PublicVisibility : JoinRuleItem {
data object Public : PublicVisibility data object Public : PublicVisibility
data object AskToJoin : PublicVisibility data object AskToJoin : PublicVisibility
data class Restricted(val parentSpaceId: RoomId) : PublicVisibility
data class AskToJoinRestricted(val parentSpaceId: RoomId) : PublicVisibility
} }
/** /**
* Transforms a [JoinRuleItem] option into a [JoinRule]. * Transforms a [JoinRuleItem] option into a [JoinRule].
*/ */
fun toJoinRule(): JoinRule = when (this) { fun toJoinRule(): JoinRule = when (this) {
Private -> JoinRule.Invite PrivateVisibility.Private -> JoinRule.Invite
is PrivateVisibility.Restricted -> JoinRule.Restricted(persistentListOf(AllowRule.RoomMembership(parentSpaceId)))
is PrivateVisibility.AskToJoinRestricted -> JoinRule.KnockRestricted(persistentListOf(AllowRule.RoomMembership(parentSpaceId)))
PublicVisibility.Public -> JoinRule.Public PublicVisibility.Public -> JoinRule.Public
PublicVisibility.AskToJoin -> JoinRule.Knock PublicVisibility.AskToJoin -> JoinRule.Knock
is PublicVisibility.Restricted -> JoinRule.Restricted(persistentListOf(AllowRule.RoomMembership(parentSpaceId)))
is PublicVisibility.AskToJoinRestricted -> JoinRule.KnockRestricted(persistentListOf(AllowRule.RoomMembership(parentSpaceId)))
} }
} }

View file

@ -12,7 +12,7 @@ import java.util.Optional
sealed interface RoomVisibilityState { sealed interface RoomVisibilityState {
val joinRuleItem: JoinRuleItem val joinRuleItem: JoinRuleItem
data class Private(override val joinRuleItem: JoinRuleItem.Private = JoinRuleItem.Private) : RoomVisibilityState data class Private(override val joinRuleItem: JoinRuleItem.PrivateVisibility) : RoomVisibilityState
data class Public( data class Public(
val roomAddress: RoomAddress, val roomAddress: RoomAddress,

View file

@ -8,7 +8,6 @@
package io.element.android.features.createroom.impl.configureroom package io.element.android.features.createroom.impl.configureroom
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -21,7 +20,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.theme.ElementTheme
import io.element.android.features.createroom.impl.R import io.element.android.features.createroom.impl.R
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.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
@ -30,7 +29,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.list.ListItemContent
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.IconSource
import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListSectionHeader import io.element.android.libraries.designsystem.theme.components.ListSectionHeader
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
@ -55,6 +53,7 @@ internal fun SelectParentSpaceOptions(
var displaySelectSpaceBottomSheet by remember { mutableStateOf(false) } var displaySelectSpaceBottomSheet by remember { mutableStateOf(false) }
ConfigureRoomOptions( ConfigureRoomOptions(
title = stringResource(CommonStrings.common_space), title = stringResource(CommonStrings.common_space),
hasDivider = false,
modifier = modifier modifier = modifier
) { ) {
ListItem( ListItem(
@ -62,22 +61,16 @@ internal fun SelectParentSpaceOptions(
Text( Text(
text = selectedSpace?.displayName text = selectedSpace?.displayName
?: stringResource(R.string.screen_create_room_space_selection_no_space_title), ?: stringResource(R.string.screen_create_room_space_selection_no_space_title),
maxLines = 1 maxLines = 1,
color = ElementTheme.colors.textPrimary
) )
}, },
supportingContent = { supportingContent = selectedSpace?.canonicalAlias?.let { alias ->
Text( {
text = if (selectedSpace != null) { Text(text = alias.value, maxLines = 1)
selectedSpace.canonicalAlias?.value.orEmpty() }
} else {
stringResource(R.string.screen_create_room_space_selection_no_space_description)
},
maxLines = 1
)
}, },
leadingContent = if (selectedSpace == null) { leadingContent = selectedSpace?.let {
ListItemContent.Icon(IconSource.Vector(CompoundIcons.Home()))
} else {
ListItemContent.Custom({ ListItemContent.Custom({
val avatarData = AvatarData( val avatarData = AvatarData(
id = selectedSpace.roomId.value, id = selectedSpace.roomId.value,
@ -119,7 +112,7 @@ internal fun SelectParentSpaceOptions(
} }
@Composable @Composable
private fun ColumnScope.SelectParentSpaceBottomSheet( private fun SelectParentSpaceBottomSheet(
spaces: ImmutableList<SpaceRoom>, spaces: ImmutableList<SpaceRoom>,
selectedSpace: SpaceRoom?, selectedSpace: SpaceRoom?,
onSelectSpace: (SpaceRoom?) -> Unit, onSelectSpace: (SpaceRoom?) -> Unit,
@ -133,19 +126,10 @@ private fun ColumnScope.SelectParentSpaceBottomSheet(
ListItem( ListItem(
headlineContent = { headlineContent = {
Text( Text(
stringResource(R.string.screen_create_room_space_selection_no_space_title), text = stringResource(R.string.screen_create_room_space_selection_no_space_option),
maxLines = 1 maxLines = 1
) )
}, },
supportingContent = {
Text(
stringResource(R.string.screen_create_room_space_selection_no_space_description),
maxLines = 1
)
},
leadingContent = ListItemContent.Icon(
IconSource.Vector(CompoundIcons.Home())
),
trailingContent = ListItemContent.RadioButton( trailingContent = ListItemContent.RadioButton(
selected = selectedSpace == null selected = selectedSpace == null
), ),
@ -157,29 +141,31 @@ private fun ColumnScope.SelectParentSpaceBottomSheet(
ListItem( ListItem(
headlineContent = { headlineContent = {
Text( Text(
space.displayName, text = space.displayName,
maxLines = 1 maxLines = 1
) )
}, },
supportingContent = { supportingContent = space.canonicalAlias?.let { alias ->
Text( {
space.canonicalAlias?.value.orEmpty(), Text(
maxLines = 1 text = alias.value,
) maxLines = 1
)
}
}, },
leadingContent = ListItemContent.Custom({ leadingContent = ListItemContent.Custom({
val avatarData = val avatarData =
AvatarData( AvatarData(
id = space.roomId.value, id = space.roomId.value,
name = space.displayName, name = space.displayName,
url = space.avatarUrl, url = space.avatarUrl,
size = AvatarSize.SelectParentSpace, size = AvatarSize.SelectParentSpace,
)
Avatar(
avatarData = avatarData,
avatarType = AvatarType.Space()
) )
}), Avatar(
avatarData = avatarData,
avatarType = AvatarType.Space()
)
}),
trailingContent = ListItemContent.RadioButton( trailingContent = ListItemContent.RadioButton(
selected = selectedSpace == space selected = selectedSpace == space
), ),
@ -201,7 +187,8 @@ internal fun SelectParentSpaceBottomSheetPreview() =
canonicalAlias = RoomAlias( canonicalAlias = RoomAlias(
"#a-room-alias:example.org" "#a-room-alias:example.org"
) )
) ),
aSpaceRoom()
), ),
selectedSpace = null, selectedSpace = null,
) {} ) {}

View file

@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.RoomInfo
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.room.alias.RoomAliasHelper import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
@ -89,7 +90,7 @@ class ConfigureRoomPresenterTest {
assertThat(initialState.config.topic).isNull() assertThat(initialState.config.topic).isNull()
assertThat(initialState.config.invites).isEmpty() assertThat(initialState.config.invites).isEmpty()
assertThat(initialState.config.avatarUri).isNull() assertThat(initialState.config.avatarUri).isNull()
assertThat(initialState.config.visibilityState).isEqualTo(RoomVisibilityState.Private()) assertThat(initialState.config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(initialState.createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(initialState.homeserverName).isEqualTo("matrix.org") assertThat(initialState.homeserverName).isEqualTo("matrix.org")
} }
@ -234,7 +235,8 @@ class ConfigureRoomPresenterTest {
matrixClient.givenCreateRoomResult(createRoomResult) matrixClient.givenCreateRoomResult(createRoomResult)
val parentSpace = aSpaceRoom() // Use a public parent space so AskToJoin is a valid option
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace)) initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace) assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
@ -275,7 +277,8 @@ class ConfigureRoomPresenterTest {
matrixClient.givenCreateRoomResult(createRoomResult) matrixClient.givenCreateRoomResult(createRoomResult)
val parentSpace = aSpaceRoom() // Use a public parent space so AskToJoin is a valid option
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace)) initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace) assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
@ -484,16 +487,19 @@ class ConfigureRoomPresenterTest {
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java) assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
// Then check changing the parent space resets it to private // Then check changing the parent space resets it to private
// (via LaunchedEffect fallback since Public is not in availableJoinRules for non-public parent)
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom())) initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private()) skipItems(1) // Skip intermediate state
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
// If we change the join rule back to public // If we change the join rule back to public
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public)) initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java) skipItems(1) // Skip intermediate state (Public is still invalid)
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
// Then remove the parent space, it'll be private again // Then remove the parent space, the join rule stays private
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(null)) initialState.eventSink(ConfigureRoomEvents.SetParentSpace(null))
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private()) assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
} }
} }

View file

@ -18,12 +18,12 @@ import org.junit.Test
class JoinRuleItemTest { class JoinRuleItemTest {
@Test @Test
fun `toJoinRule works as expected`() { fun `toJoinRule works as expected`() {
assertThat(JoinRuleItem.Private.toJoinRule()).isEqualTo(JoinRule.Invite) assertThat(JoinRuleItem.PrivateVisibility.Private.toJoinRule()).isEqualTo(JoinRule.Invite)
assertThat(JoinRuleItem.PublicVisibility.Public.toJoinRule()).isEqualTo(JoinRule.Public) assertThat(JoinRuleItem.PublicVisibility.Public.toJoinRule()).isEqualTo(JoinRule.Public)
assertThat(JoinRuleItem.PublicVisibility.AskToJoin.toJoinRule()).isEqualTo(JoinRule.Knock) assertThat(JoinRuleItem.PublicVisibility.AskToJoin.toJoinRule()).isEqualTo(JoinRule.Knock)
assertThat(JoinRuleItem.PublicVisibility.Restricted(A_ROOM_ID).toJoinRule()) assertThat(JoinRuleItem.PrivateVisibility.Restricted(A_ROOM_ID).toJoinRule())
.isEqualTo(JoinRule.Restricted(persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)))) .isEqualTo(JoinRule.Restricted(persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))))
assertThat(JoinRuleItem.PublicVisibility.AskToJoinRestricted(A_ROOM_ID).toJoinRule()) assertThat(JoinRuleItem.PrivateVisibility.AskToJoinRestricted(A_ROOM_ID).toJoinRule())
.isEqualTo(JoinRule.KnockRestricted(persistentListOf(AllowRule.RoomMembership(A_ROOM_ID)))) .isEqualTo(JoinRule.KnockRestricted(persistentListOf(AllowRule.RoomMembership(A_ROOM_ID))))
} }
} }

View file

@ -76,6 +76,7 @@ fun HomeSpacesView(
item { item {
SpaceHeaderView( SpaceHeaderView(
avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader), avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader),
alias = space.spaceRoom.canonicalAlias,
name = space.spaceRoom.displayName, name = space.spaceRoom.displayName,
topic = space.spaceRoom.topic, topic = space.spaceRoom.topic,
visibility = space.spaceRoom.visibility, visibility = space.spaceRoom.visibility,

View file

@ -41,8 +41,8 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.invite.api.InviteData import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewAliasAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
@ -514,7 +514,7 @@ private fun IncompleteContent(
title = { title = {
when (roomIdOrAlias) { when (roomIdOrAlias) {
is RoomIdOrAlias.Alias -> { is RoomIdOrAlias.Alias -> {
RoomPreviewSubtitleAtom(roomIdOrAlias.identifier) RoomPreviewAliasAtom(roomIdOrAlias.identifier)
} }
is RoomIdOrAlias.Id -> { is RoomIdOrAlias.Id -> {
PlaceholderAtom(width = 200.dp, height = 22.dp) PlaceholderAtom(width = 200.dp, height = 22.dp)
@ -566,13 +566,12 @@ private fun DefaultLoadedContent(
} }
}, },
subtitle = { subtitle = {
when { if (contentState.alias != null) {
contentState.details is LoadedDetails.Space -> { RoomPreviewAliasAtom(contentState.alias.value)
SpaceInfoRow(visibility = SpaceRoomVisibility.fromJoinRule(contentState.joinRule)) }
} if (contentState.details is LoadedDetails.Space) {
contentState.alias != null -> { Spacer(Modifier.height(8.dp))
RoomPreviewSubtitleAtom(contentState.alias.value) SpaceInfoRow(visibility = SpaceRoomVisibility.fromJoinRule(contentState.joinRule))
}
} }
}, },
description = { description = {

View file

@ -25,7 +25,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewAliasAtom
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@ -54,7 +54,7 @@ fun RoomAliasResolverView(
containerColor = Color.Transparent, containerColor = Color.Transparent,
contentPadding = PaddingValues( contentPadding = PaddingValues(
horizontal = 16.dp, horizontal = 16.dp,
vertical = 32.dp vertical = 24.dp
), ),
topBar = { topBar = {
RoomAliasResolverTopBar(onBackClick = onBackClick) RoomAliasResolverTopBar(onBackClick = onBackClick)
@ -121,7 +121,7 @@ private fun RoomAliasResolverContent(
PlaceholderAtom(width = AvatarSize.RoomPreviewHeader.dp, height = AvatarSize.RoomPreviewHeader.dp) PlaceholderAtom(width = AvatarSize.RoomPreviewHeader.dp, height = AvatarSize.RoomPreviewHeader.dp)
}, },
title = { title = {
RoomPreviewSubtitleAtom(roomAlias.value) RoomPreviewAliasAtom(roomAlias.value)
}, },
subtitle = { subtitle = {
if (isLoading) { if (isLoading) {

View file

@ -6,6 +6,8 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(FlowPreview::class)
package io.element.android.features.space.impl.root package io.element.android.features.space.impl.root
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -47,10 +49,13 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
@Inject @Inject
class SpacePresenter( class SpacePresenter(
@ -80,13 +85,16 @@ class SpacePresenter(
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val hasMoreToLoad by remember { val hasMoreToLoad by remember {
spaceRoomList.paginationStatusFlow.mapState { status -> spaceRoomList.paginationStatusFlow
when (status) { .mapState { status ->
is SpaceRoomList.PaginationStatus.Idle -> status.hasMoreToLoad when (status) {
SpaceRoomList.PaginationStatus.Loading -> true is SpaceRoomList.PaginationStatus.Idle -> status.hasMoreToLoad
SpaceRoomList.PaginationStatus.Loading -> true
}
} }
} // Debounce to give more time for spaceRoomList to updates
}.collectAsState() .debounce(100.milliseconds)
}.collectAsState(true)
val permissions by room.permissionsAsState(SpacePermissions.DEFAULT) { perms -> val permissions by room.permissionsAsState(SpacePermissions.DEFAULT) { perms ->
perms.spacePermissions() perms.spacePermissions()

View file

@ -50,6 +50,7 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.space.impl.R import io.element.android.features.space.impl.R
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule import io.element.android.libraries.designsystem.atomic.molecules.InviteButtonsRowMolecule
import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.BigIcon
@ -76,6 +77,7 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalDivi
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
@ -170,6 +172,7 @@ fun SpaceView(
state.eventSink(SpaceEvents.ShowTopicViewer(topic)) state.eventSink(SpaceEvents.ShowTopicViewer(topic))
}, },
onCreateRoomClick = onCreateRoomClick, onCreateRoomClick = onCreateRoomClick,
onAddRoomClick = onAddRoomClick,
) )
JoinFailuresEffect( JoinFailuresEffect(
hasAnyFailure = state.hasAnyJoinFailures, hasAnyFailure = state.hasAnyJoinFailures,
@ -243,6 +246,7 @@ private fun SpaceViewContent(
onRoomClick: (spaceRoom: SpaceRoom) -> Unit, onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
onTopicClick: (String) -> Unit, onTopicClick: (String) -> Unit,
onCreateRoomClick: () -> Unit, onCreateRoomClick: () -> Unit,
onAddRoomClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn(modifier.fillMaxSize()) { LazyColumn(modifier.fillMaxSize()) {
@ -256,6 +260,7 @@ private fun SpaceViewContent(
Column { Column {
SpaceHeaderView( SpaceHeaderView(
avatarData = spaceInfo.getAvatarData(AvatarSize.SpaceHeader), avatarData = spaceInfo.getAvatarData(AvatarSize.SpaceHeader),
alias = spaceInfo.canonicalAlias,
name = spaceInfo.name, name = spaceInfo.name,
topic = spaceInfo.topic, topic = spaceInfo.topic,
topicMaxLines = 2, topicMaxLines = 2,
@ -271,7 +276,10 @@ private fun SpaceViewContent(
if (state.children.isEmpty() && state.canEditSpaceGraph && !state.hasMoreToLoad) { if (state.children.isEmpty() && state.canEditSpaceGraph && !state.hasMoreToLoad) {
item { item {
EmptySpaceView(onCreateRoomClick = onCreateRoomClick) EmptySpaceView(
onCreateRoomClick = onCreateRoomClick,
onAddRoomClick = onAddRoomClick,
)
} }
} else { } else {
itemsIndexed( itemsIndexed(
@ -332,7 +340,10 @@ private fun SpaceViewContent(
} }
@Composable @Composable
private fun EmptySpaceView(onCreateRoomClick: () -> Unit) { private fun EmptySpaceView(
onCreateRoomClick: () -> Unit,
onAddRoomClick: () -> Unit,
) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(bottom = 24.dp), modifier = Modifier.padding(bottom = 24.dp),
@ -340,15 +351,25 @@ private fun EmptySpaceView(onCreateRoomClick: () -> Unit) {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
title = stringResource(R.string.screen_space_empty_state_title), title = stringResource(R.string.screen_space_empty_state_title),
subTitle = null, subTitle = null,
iconStyle = BigIcon.Style.Default(CompoundIcons.Room()), iconStyle = BigIcon.Style.Default(vectorIcon = CompoundIcons.Room(), usePrimaryTint = true),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.padding(top = 40.dp, start = 24.dp, end = 24.dp, bottom = 24.dp), .padding(top = 40.dp, start = 24.dp, end = 24.dp, bottom = 24.dp),
) )
Button( ButtonColumnMolecule(
text = stringResource(R.string.screen_space_add_room_action), modifier = Modifier.padding(horizontal = 16.dp)
leadingIcon = IconSource.Vector(CompoundIcons.Plus()), ) {
onClick = onCreateRoomClick, Button(
) text = stringResource(CommonStrings.action_add_existing_rooms),
leadingIcon = IconSource.Vector(CompoundIcons.Plus()),
onClick = onAddRoomClick,
modifier = Modifier.fillMaxWidth()
)
OutlinedButton(
text = stringResource(CommonStrings.action_create_room),
onClick = onCreateRoomClick,
modifier = Modifier.fillMaxWidth()
)
}
} }
} }

View file

@ -15,7 +15,6 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.space.impl.R
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.spaces.SpaceRoom import io.element.android.libraries.matrix.api.spaces.SpaceRoom
@ -215,9 +214,25 @@ class SpaceViewTest {
), ),
onCreateRoomClick = onCreateRoomClick, onCreateRoomClick = onCreateRoomClick,
) )
rule.clickOn(R.string.screen_space_add_room_action) rule.clickOn(CommonStrings.action_create_room)
onCreateRoomClick.assertions().isCalledOnce() onCreateRoomClick.assertions().isCalledOnce()
} }
@Test
fun `clicking add existing room button calls the expected callback`() {
val onAddRoomClick = lambdaRecorder<Unit> { }
rule.setSpaceView(
aSpaceState(
children = emptyList(),
hasMoreToLoad = false,
isManageMode = true,
canManageRooms = true,
),
onAddRoomClick = onAddRoomClick,
)
rule.clickOn(CommonStrings.action_add_existing_rooms)
onAddRoomClick.assertions().isCalledOnce()
}
} }
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceView( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpaceView(

View file

@ -0,0 +1,83 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.atoms
import android.content.ClipData
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.toClipEntry
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
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.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@Composable
fun RoomPreviewAliasAtom(
alias: String,
modifier: Modifier = Modifier,
copiable: Boolean = true
) {
val clipboard: Clipboard = LocalClipboard.current
val coroutineScope = rememberCoroutineScope()
Row(
modifier = modifier
.clickable(enabled = copiable) {
coroutineScope.launch {
val clipData = ClipData.newPlainText(alias, alias)
clipboard.setClipEntry(clipData.toClipEntry())
}
},
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(weight = 1f, fill = false),
text = alias,
style = ElementTheme.typography.fontBodyLgRegular,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = ElementTheme.colors.textSecondary,
)
if (copiable) {
Icon(
imageVector = CompoundIcons.Copy(),
contentDescription = stringResource(id = CommonStrings.action_copy),
tint = ElementTheme.colors.iconSecondaryAlpha,
modifier = Modifier.size(ElementTheme.typography.fontBodyLgRegular.fontSize.toDp())
)
}
}
}
@PreviewsDayNight
@Composable
internal fun RoomPreviewAliasAtomPreview() = ElementPreview {
RoomPreviewAliasAtom(
alias = "#room-alias:matrix.org",
copiable = true
)
}

View file

@ -1,26 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.atoms
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun RoomPreviewSubtitleAtom(subtitle: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = subtitle,
style = ElementTheme.typography.fontBodyLgRegular,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textSecondary,
)
}

View file

@ -9,6 +9,7 @@
package io.element.android.libraries.designsystem.atomic.organisms package io.element.android.libraries.designsystem.atomic.organisms
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -19,12 +20,12 @@ import androidx.compose.ui.unit.dp
@Composable @Composable
fun RoomPreviewOrganism( fun RoomPreviewOrganism(
avatar: @Composable () -> Unit, avatar: @Composable ColumnScope.() -> Unit,
title: @Composable () -> Unit, title: @Composable ColumnScope.() -> Unit,
subtitle: @Composable () -> Unit, subtitle: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
description: @Composable (() -> Unit)? = null, description: @Composable (ColumnScope.() -> Unit)? = null,
memberCount: @Composable (() -> Unit)? = null, memberCount: @Composable (ColumnScope.() -> Unit)? = null,
) { ) {
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.designsystem.components.avatar.internal package io.element.android.libraries.designsystem.components.avatar.internal
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -16,6 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.avatar.anAvatarData import io.element.android.libraries.designsystem.components.avatar.anAvatarData
@ -34,19 +36,26 @@ internal fun SpaceAvatar(
contentDescription: String? = null, contentDescription: String? = null,
) { ) {
val size = forcedAvatarSize ?: avatarData.size.dp val size = forcedAvatarSize ?: avatarData.size.dp
val avatarShape = avatarType.avatarShape(size)
val commonModifier = modifier
.border(
width = 1.dp,
color = ElementTheme.colors.iconQuaternaryAlpha,
shape = avatarShape,
)
when { when {
avatarType.isTombstoned -> TombstonedRoomAvatar( avatarType.isTombstoned -> TombstonedRoomAvatar(
size = size, size = size,
avatarShape = avatarType.avatarShape(size), avatarShape = avatarShape,
modifier = modifier, modifier = commonModifier,
contentDescription = contentDescription, contentDescription = contentDescription,
) )
else -> InitialOrImageAvatar( else -> InitialOrImageAvatar(
avatarData = avatarData, avatarData = avatarData,
hideAvatarImage = hideAvatarImage, hideAvatarImage = hideAvatarImage,
avatarShape = avatarType.avatarShape(size), avatarShape = avatarShape,
forcedAvatarSize = forcedAvatarSize, forcedAvatarSize = forcedAvatarSize,
modifier = modifier, modifier = commonModifier,
contentDescription = contentDescription, contentDescription = contentDescription,
) )
} }

View file

@ -14,12 +14,12 @@ import io.element.android.libraries.matrix.api.room.join.JoinRule
sealed interface SpaceRoomVisibility { sealed interface SpaceRoomVisibility {
data object Private : SpaceRoomVisibility data object Private : SpaceRoomVisibility
data object Public : SpaceRoomVisibility data object Public : SpaceRoomVisibility
data object Restricted : SpaceRoomVisibility data object SpaceMembers : SpaceRoomVisibility
companion object { companion object {
fun fromJoinRule(joinRule: JoinRule?): SpaceRoomVisibility = when (joinRule) { fun fromJoinRule(joinRule: JoinRule?): SpaceRoomVisibility = when (joinRule) {
JoinRule.Public -> Public JoinRule.Public -> Public
is JoinRule.Restricted, is JoinRule.KnockRestricted -> Restricted is JoinRule.Restricted, is JoinRule.KnockRestricted -> SpaceMembers
// Else fallback to Private // Else fallback to Private
else -> Private else -> Private
} }

View file

@ -9,13 +9,17 @@
package io.element.android.libraries.matrix.ui.components package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewAliasAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
@ -26,6 +30,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.avatar.anAvatarData import io.element.android.libraries.designsystem.components.avatar.anAvatarData
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.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.spaces.SpaceRoomVisibility import io.element.android.libraries.matrix.api.spaces.SpaceRoomVisibility
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -39,6 +44,7 @@ import kotlinx.collections.immutable.persistentListOf
fun SpaceHeaderView( fun SpaceHeaderView(
avatarData: AvatarData, avatarData: AvatarData,
name: String?, name: String?,
alias: RoomAlias?,
topic: String?, topic: String?,
visibility: SpaceRoomVisibility, visibility: SpaceRoomVisibility,
heroes: ImmutableList<MatrixUser>, heroes: ImmutableList<MatrixUser>,
@ -66,7 +72,15 @@ fun SpaceHeaderView(
} }
}, },
subtitle = { subtitle = {
SpaceInfoRow(visibility = visibility) Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
if (alias != null) {
RoomPreviewAliasAtom(alias = alias.value)
}
SpaceInfoRow(visibility = visibility)
}
}, },
description = if (topic.isNullOrBlank()) { description = if (topic.isNullOrBlank()) {
null null
@ -100,6 +114,7 @@ internal fun SpaceHeaderViewPreview() = ElementPreview {
url = "anUrl", url = "anUrl",
size = AvatarSize.SpaceHeader, size = AvatarSize.SpaceHeader,
), ),
alias = RoomAlias("#spaceAlias:matrix.org"),
name = "Space name", name = "Space name",
topic = "Space topic: " + LoremIpsum(40).values.first(), topic = "Space topic: " + LoremIpsum(40).values.first(),
topicMaxLines = 2, topicMaxLines = 2,

View file

@ -117,7 +117,7 @@ internal fun SpaceInfoRowPreview() = ElementPreview {
visibility = SpaceRoomVisibility.Public visibility = SpaceRoomVisibility.Public
) )
SpaceInfoRow( SpaceInfoRow(
visibility = SpaceRoomVisibility.Restricted visibility = SpaceRoomVisibility.SpaceMembers
) )
} }
} }

View file

@ -24,11 +24,9 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.ripple import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -100,22 +98,16 @@ fun SpaceRoomItemView(
showIndicator = showUnreadIndicator showIndicator = showUnreadIndicator
) )
Spacer(modifier = Modifier.height(1.dp)) Spacer(modifier = Modifier.height(1.dp))
SubtitleRow( VisibilityRow(visibility = spaceRoom.visibility)
visibilityIcon = spaceRoom.visibilityIcon(),
subtitle = spaceRoom.subtitle()
)
Spacer(modifier = Modifier.height(1.dp)) Spacer(modifier = Modifier.height(1.dp))
val info = spaceRoom.info() Text(
if (info.isNotBlank()) { modifier = Modifier.weight(1f),
Text( style = ElementTheme.typography.fontBodyMdRegular,
modifier = Modifier.weight(1f), text = pluralStringResource(CommonPlurals.common_member_count, spaceRoom.numJoinedMembers, spaceRoom.numJoinedMembers),
style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textSecondary,
text = info, maxLines = 1,
color = ElementTheme.colors.textSecondary, overflow = TextOverflow.Ellipsis
maxLines = 1, )
overflow = TextOverflow.Ellipsis
)
}
} }
if (bottomAction != null) { if (bottomAction != null) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
@ -129,29 +121,26 @@ fun SpaceRoomItemView(
} }
@Composable @Composable
private fun SubtitleRow( private fun VisibilityRow(
visibilityIcon: ImageVector?, visibility: SpaceRoomVisibility,
subtitle: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
if (visibilityIcon != null) { Icon(
Icon( modifier = Modifier
modifier = Modifier .size(16.dp)
.size(16.dp) .padding(end = 4.dp),
.padding(end = 4.dp), imageVector = visibility.icon,
imageVector = visibilityIcon, contentDescription = null,
contentDescription = null, tint = ElementTheme.colors.iconTertiary,
tint = ElementTheme.colors.iconTertiary, )
)
}
Text( Text(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyMdRegular,
text = subtitle, text = visibility.label,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@ -219,36 +208,6 @@ private fun SpaceRoomItemScaffold(
} }
} }
@Composable
@ReadOnlyComposable
private fun SpaceRoom.subtitle(): String {
return if (isSpace) {
visibility.label
} else {
pluralStringResource(CommonPlurals.common_member_count, numJoinedMembers, numJoinedMembers)
}
}
@Composable
@ReadOnlyComposable
private fun SpaceRoom.info(): String {
return if (isSpace) {
pluralStringResource(CommonPlurals.common_member_count, numJoinedMembers, numJoinedMembers)
} else {
topic.orEmpty()
}
}
@Composable
private fun SpaceRoom.visibilityIcon(): ImageVector? {
// Don't show any icon for restricted rooms as it's the default and would add noise
return if (visibility == SpaceRoomVisibility.Restricted) {
null
} else {
visibility.icon
}
}
@Composable @Composable
@PreviewsDayNight @PreviewsDayNight
internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class) spaceRoom: SpaceRoom) = ElementPreview { internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class) spaceRoom: SpaceRoom) = ElementPreview {

View file

@ -32,7 +32,7 @@ val SpaceRoomVisibility.icon: ImageVector
return when (this) { return when (this) {
SpaceRoomVisibility.Private -> CompoundIcons.LockSolid() SpaceRoomVisibility.Private -> CompoundIcons.LockSolid()
SpaceRoomVisibility.Public -> CompoundIcons.Public() SpaceRoomVisibility.Public -> CompoundIcons.Public()
SpaceRoomVisibility.Restricted -> CompoundIcons.Space() SpaceRoomVisibility.SpaceMembers -> CompoundIcons.Space()
} }
} }
@ -41,8 +41,8 @@ val SpaceRoomVisibility.label: String
@ReadOnlyComposable @ReadOnlyComposable
get() { get() {
return when (this) { return when (this) {
SpaceRoomVisibility.Private -> stringResource(CommonStrings.common_private_space) SpaceRoomVisibility.Private -> stringResource(CommonStrings.common_private)
SpaceRoomVisibility.Public -> stringResource(CommonStrings.common_public_space) SpaceRoomVisibility.Public -> stringResource(CommonStrings.common_public)
SpaceRoomVisibility.Restricted -> stringResource(CommonStrings.common_shared_space) SpaceRoomVisibility.SpaceMembers -> stringResource(CommonStrings.common_space_members)
} }
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:20c185481e6a13fbccae6e4d8c02752222f8cfcc5c03398c255666beb5df5cc4 oid sha256:8a5a6c238f044364415ec56a4e44eb89803fbdbd0a5c0043e9119b916bfeef56
size 34243 size 34306

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2d0fb8d4c5acde1b03908f15eb959d3e544917a53c529f5971804eb674f989fa oid sha256:8523912153c301442253a41c236ada8ba934ca8079b7c1d9463dbfdb20325649
size 41893 size 41892

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a601ab64bac5f2c11480892e4b81dcaf3b9fe72afd222f6f42d1e5f577220a8c oid sha256:ce22c06eb1d6140dc852ef76a4f6acb7dfc175019fa1d33c1695a551aaaecb05
size 42786 size 42785

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f8a2dd4b01f32b9325e9adf8340f18b6776ff20c06f4e27590bde207d802b3c8 oid sha256:e3f6e32f8bc237e14e6ad4ca1da93e70631355b062ad8de698b22e36f19a5fce
size 44542 size 44542

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2d0fb8d4c5acde1b03908f15eb959d3e544917a53c529f5971804eb674f989fa oid sha256:8523912153c301442253a41c236ada8ba934ca8079b7c1d9463dbfdb20325649
size 41893 size 41892

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f0f2a05605785d5a9484e6e34b03c6074125927f6b8130f5e60f54e3922d7bc4 oid sha256:3ce5823f8724dc03df2a171182c3cab522c30c3cf912d3efd798c572aa80a872
size 42863 size 42860

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:356117f3617c58b79fcbe95aaba2899afdc4735d5832cc274622bc1689572a5a oid sha256:6092a495d695de848c63919121ea6199ebbacd5a996becd1ebdcf685fad21146
size 46689 size 37738

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d4bf55777af9384a34c438de10887f5b6dff107b837f4bb7a2f974644c256047 oid sha256:3fb3642812dbfac7151986fecbb7a8bdcea31f9367f901420d4003e920f4c852
size 48431 size 40893

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fd25259e89b4a8833c884727d23fb503b53099b698ff2eafd812bf70422b5cbe oid sha256:a85264a26b9db2bb9bdace44aaa0ca0a3c175a89efd442252d57ccd1ab74370a
size 35631 size 35665

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:30a4f34c42cb57b3cbd08114fef8531175890d46d0033f0dc5d1337189e33d1c oid sha256:94fd7ed41f8d38f00805aa6b7224f28a34565136c5b10319e4638c9f97a68cd9
size 43653 size 43652

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ab5d721149a96e53f850d7792653b0b71d41124c0db82d653ad79c66c72dabee oid sha256:e1b7b5086ee131e28daf6afcc6f889486b1815bf2cc8dbf83fec772ca670a902
size 44600 size 44601

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b7a8c9c3cb8f877d3dc2deb1b918666143c73aed34fdd3d3961bc2530f32d0f5 oid sha256:1857d55f616858ae0f6692b1c817f472c9695db6d63c30b9e64b6522ae813a69
size 46369 size 46369

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:30a4f34c42cb57b3cbd08114fef8531175890d46d0033f0dc5d1337189e33d1c oid sha256:94fd7ed41f8d38f00805aa6b7224f28a34565136c5b10319e4638c9f97a68cd9
size 43653 size 43652

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d3cb687ded32d38164cb67657b495958f1632457d1e49ede0d8c9f1b1f5a287b oid sha256:75a247266aecc4b08ea0e3eef37086b172e0b48df9c4e294f27a899ac52b18ca
size 44675 size 44674

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d857d4c831cd4f786b29f392ffa05716756f0cb3c59199057fed92abbd78901e oid sha256:10b1e26af1f81e0b2dc7e991c1ec5fab230edae7c1996c532011b5689bc7d27a
size 48485 size 39249

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0e95d2194cfc8b979624ce3b97250a85051d67bd8f5f61bb1e60d274c08f4649 oid sha256:b5041b3c70ef02579037940afbaf9799cef72151069940c3fcd07ed87492d1dd
size 50132 size 42533

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:27ce57d26309360b24f5b62bd45ab2bf0420a42559556105c6dd42d0d0afead9 oid sha256:e015ed8f3b90780c35fc12fee64119659e43510d379658cf46338ce3b86fc561
size 23476 size 25563

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a18eb487e585eb0a37c4a39c94d5c897c57fbcdbe450ca6b7b34b3baad6262a5 oid sha256:f912f91abf5e09b6039b3668c92f1bc3bed87b05ceb14678509b9f9a8754fe7c
size 22530 size 24998

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:98d4e9152dee861cc7afdb3efe57b086c3d9d58d38eeb1345749d31debbf6017 oid sha256:6b76e09d9138021a2dd9338c4ffc39c1d530711eb4c96343edd2450366b8c6cd
size 21266 size 22089

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c24a3b08afe224e9919dd3818c72bd2ff8dd3f38663a6465a10b301ad91e8787 oid sha256:7bc1f006db0f7763fde512dcfa1d731be8040979bdd5b44cd2ca0b70f651ffa5
size 20377 size 20984

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5ed03e5c6103dd4d96a0c4a8fda97808801f853cc3e05595e339a67e0b228027 oid sha256:bfc98a004d03c8464f7e0716122c765791b6417004a2b669a213f4ecd57004e7
size 30472 size 32416

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dfeca128a35edebe19f59749bedcd7335422377fd2627f95f81d7e3e28dd61e5 oid sha256:ad7a4ea61d969d99e4ba7406721b6d852f86b0f3fdf472b555074dc654566d48
size 18173 size 18670

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:543f80c91d04f44c4c62a06c9e44b334826d5bd92f2b17a4eeeb7e15dfbed0c1 oid sha256:def089ca29a9a53fa373c6e5890358702e596964ddc52eb1bb11aaea8f273a00
size 29510 size 31067

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8735caab11d89a689591c1d1ad5c1483d0c1fc01ce7b5933c7546ce2c2641d4a oid sha256:30158c9c805b21a1212b0621927e89ee14116d51a2604704d0736e8a07de67fc
size 17162 size 17570

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:388207cd5b424fbb95f070eac393db9330ae1b641795d1bb815874435ef9f623 oid sha256:a9e55679970ff85c3c66013eac5c755c879de18d619f0b75e0f6dc9d40fc5d72
size 89027 size 86233

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:542d8ba6a6031fe2789cf111f333eef22acf95281f57421ace2c7b5b0a599cc2 oid sha256:d7bf08648b5c3a09390e09d0ba1e34d43b5ac40e73c2be86048d80f3c78cc455
size 41140 size 39482

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:542d8ba6a6031fe2789cf111f333eef22acf95281f57421ace2c7b5b0a599cc2 oid sha256:d7bf08648b5c3a09390e09d0ba1e34d43b5ac40e73c2be86048d80f3c78cc455
size 41140 size 39482

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:134e561fc4082339725c241a79fa55e0b5b1e134c046d4454cb7a9e71ea5e1b7 oid sha256:442a1d4dcf025734b956347fe865203eefeb64c0483852771c65307de739930a
size 87174 size 84421

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c31cd78bc054610be05012cdba7eb0cbc770435b0e12bc065f6eae4a773ca39e oid sha256:20762b4fb5695b33bec82470aceb257ad75acbecad287521d8df94a96331784f
size 40121 size 38184

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c31cd78bc054610be05012cdba7eb0cbc770435b0e12bc065f6eae4a773ca39e oid sha256:20762b4fb5695b33bec82470aceb257ad75acbecad287521d8df94a96331784f
size 40121 size 38184

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d398e399f468705b9f78283535c2b0e3f44f8be0456cf99bb9b3611746cd0af2 oid sha256:c34974246d111739cb6a94443f2fa41ad439831e293fa651ba842bdc985175b3
size 54380 size 53099

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7fce6676e186e13f14ea8cc8436fdbcadcf41bf84a1999104794bd2245814337 oid sha256:352499d53b913de50890061e923c439ddacf2aed029960a4feafd7f146c59266
size 52805 size 51368

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4884f1972998a969fa3a8948494bcd0247c1149b9340b1be845ffa432a0a0d9c oid sha256:4bdf2532d697fec8ea51cb071d3ea0476dae4d238a54612120b7d528bc9ce12d
size 9978 size 10286

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dfe2f91dd046dcd7416cc2767f467d793e14b1587379e328659389515d37ff84 oid sha256:1df7c2cb351acf1ecd96ec4c654319bba4b25e3e52a8fbb3fa4c02b4b5f08c1c
size 39363 size 39544

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b57f48029efc014cd088ac6c36c3f6ecbb1b9c6c8f3ec77590112d09f700dbce oid sha256:ce2491439a30e286e18f05ddee5a5e85c4313731f5f52195c6fb5f9c5957f0b3
size 43131 size 43319

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:874a0b5d64b35c45a0293ebddbeb887377a8a40134af68828ebea0de34ca8694 oid sha256:9f7f29c9877dfcf6a7b06ae0ff002ec56de13aa2b1baf49594b2388e9b9ec06c
size 44119 size 44314

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:23c88713ba8991df8b1beff3762ebdd48f3f6e3c31ff3f7713c440bc12435114 oid sha256:3192ffaac9a7881b650fae6d241558bead45e7c646fa0fd5d5c21d22b90e4b47
size 30118 size 30308

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9683fc7b78efa029be47bdb20a2049bcddff1277f7170b67f887c0cc8ee0fb10 oid sha256:c0e479a53bca5e6fe847de449a68206037b4f1159b8780ff07e2c74f384d8387
size 34591 size 34803

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d2bf6ba4782f4c2dc891ea0410ed4797487b7f0f6292867e018b89efd33b46d8 oid sha256:0ceec3639f3aca6207d1f853c88205eb2f54acbd558842e235988dd516a37cc4
size 42478 size 42704

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6847b8f932fde9694cdcc3b911f4c90d6836ade22a2ce4fc2db7518812ef5008 oid sha256:d09d2518ce2156fb3b5ffb1e0a0d2c76a88029d622107742bd5915208cfc6e5e
size 28735 size 28928

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:41de5451185f9ebd276baa7cb06bdea542cc6d9edd53ee08717f04204a0f4963 oid sha256:e05723495b1bc12b0396e3036d488860606b5beb57363589e11db7c9f7b2867e
size 39600 size 39789

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b24654360b865e485ee280db126029a8f0851127d3c1606810a2ab11aaa0359d oid sha256:767970884d9b5464101fc15a945650978386a210f3a81ad2250a70a8504f535e
size 30585 size 30659

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7c4de5a07ef4b6c7a03db84d161a467e929dacda83bf363eef41443dad59f6c1 oid sha256:fe891574b6035f927fedf26b4e26a26df96cfb8d881e9a2b39925aec1b1104e4
size 33002 size 33195

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7240e42cb7593cde2c9f4ebb9e3808f8ebe1de2251518074fdafd96aaee3a09d oid sha256:4f69db01001fd8204afd3f2f1375784ed8053a2c302489ed90d8661148ee42ae
size 39424 size 39617

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fc05216680219eadfa9df834287831d13c3f37ce6eef45b1b49c26124b562f19 oid sha256:aa9b4c071e95d809b400ab2a1b19191689c20cb5d0d6502620f35a960a3ceb83
size 29089 size 29313

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:22f979dfb2c48d2d03ee439fde88e9fc355a667860c5c55574dc57644e1a9aac oid sha256:1d4b9e06fbfa9ff90f51d5942e80bb3f5c00d731f36cc3b519b513332caf56c0
size 35398 size 35233

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ec6efb77f63ec2e06866aa56c1a8eaa563f651a80144e5da1c4d27ff59275411 oid sha256:d9c851f5baa8880390deaae0c54d0506f77f88129e5c276660e0f5ef1e159642
size 9829 size 10044

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f1c917c487c0eb32ca844c24ae725921174a8b0446fda2d1263879ce19d90d3f oid sha256:8f5d65ee186f09ff9c7750eb931c4674efb2c22281ab4d5ba4190ddfdd2c5c94
size 39060 size 39282

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:50c653b8a22a8bdde89f0729568858810991bf43e0628cf65989b487519ce488 oid sha256:d20973d72a98df4a2c76e3a81ec8d1e912f4f488cdfe13c1281c945e42a37a02
size 42639 size 42877

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a92f0c0aab2a62bd94f5701b308ae48fab3f342ffb4923071ace642c3b32ae74 oid sha256:7314b927a0211aa1ddae175c999c6e9fd666d9d4390dc3895198414405bc1abc
size 43562 size 43800

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a457e82ef171deb34b17c70494cd686cab16a5550f17edcbf72d27752f15435d oid sha256:041b36720ac5db3bf85576db8165929e7997df7976272bbf77f0e3e60271471d
size 29606 size 29837

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:12fa84ff4f3766a34de6eae4d8158e5090d2cf692afbc0cea973c7393ad99729 oid sha256:75ac6b746a460063e22b9b21cca10268daca5a7f2b0a9139cad1aca04185c1c8
size 33774 size 34009

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fbe6574b3732b8d3ccde1f8188c6385f92b1109e058e0ff256d5e48451e681b0 oid sha256:c05c3ba3c5a7c8c85ffe49c501098d5f1ded6011f7f1dc8230dea8e072438139
size 42110 size 42353

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0fdb9e57ee8cd20345f336d38f8be1e3c8d5205ba3854f4ffaaeb3294512df14 oid sha256:887cadea9d50c14e36a9a8dd449a4deab978c84190650f4887dab687077e3d9f
size 28613 size 28875

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a9a9aacc227163f349dd0c3525f5f2bdb7959e9352c27f96160c4711de0dc8a9 oid sha256:433bf40feee3d3ecb5563437eea2620dee42508c99fe601b699e5e978ab86343
size 38443 size 38671

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:69534c11f667a064d5d50a98770cdfc8e82f0988123a7a3ea3e285ef6f94725f oid sha256:9dca4ebe0873cdbec589272b80ea6adf56a9bf870c82495d836675faf25478b5
size 28895 size 28981

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3a7f6665c335429fc21f8868583022b82cb4ac94fc9cd4b2150b8c5e371c0fd1 oid sha256:ec91c04137bdb66020f82ad9a1d309c5714d0dacea0005cd998ee2f4adaf22d5
size 31959 size 32209

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:333e605408b2373140bddd69d0ae07d3be531e90ecd36f0d594886465711e50c oid sha256:b6d880dc8d13df730c0db5c7be130354b5f78ca6a582c973295a56b9b9fbd953
size 38675 size 38911

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1262b7be57eda70a38d2e2a205c406688c0c306751c460a5493ec3cf812fed6d oid sha256:9b2466cee9954445038fb31c337aec4906dbbd9ec91cdabdb085fd0c14bdb979
size 26812 size 26979

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:31d6232428dd0fe61b41c645b86ce10dc6a2f0455785ac114808fa95c66c36ba oid sha256:de906db5c115ed3f4e92d235d869346ff57fc3139a28683e345b48e5f4a0a0ff
size 35069 size 34665

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0e9ced8663df865c6bdce2e2c05bdf11a48f7b8273a0d34ed7da775bce87568a oid sha256:9697082874aae750b9e0a88f8c3ad83ce21860af7099e76e72a57a5ee4305989
size 8428 size 8669

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:56c9ac5e7b9d851e662e4544f961ca82685e1f4c93b3df84f44426e4ab40ac73 oid sha256:aa9b4c071e95d809b400ab2a1b19191689c20cb5d0d6502620f35a960a3ceb83
size 29166 size 29313

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:97a73b393c4aa2eaebc39809cc2cbc368dec4baf1d95f08b118b586a60aca841 oid sha256:ac69126e44e60c1223c7775260dd3302de500dedd345ffb8f79bb07604e25223
size 23997 size 24138

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:38b209ff4fe8aca1f46b3ce1d140c1a83ca1620affec81909eb2916528d9974c oid sha256:67c48295ecdb460323ac959bb1d4937e1d43e9dc0d0328c052a893df6f635813
size 8250 size 8436

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:44492987b98ee92ddc9901ed766fdbecc346bccb7c10e0ee942b7ea79bd481da oid sha256:9b2466cee9954445038fb31c337aec4906dbbd9ec91cdabdb085fd0c14bdb979
size 26789 size 26979

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8217409aaf7e1b3960f0af4491b71db2f7fd59fdb58488c366e8d6f485955fe0 oid sha256:44dadcebc45f3b591e461ede09cc3f2d4804f6f5224bf8dd5a1db2028d424610
size 21653 size 21859

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:144ec8da9382f33c7566452515a379eb7fec3e7c1cbd1c6c79a871e12093e6ee oid sha256:0a4d3666020f8ce450587ab8511ef74bda38e24b4b25936d6827444d49a988ba
size 48043 size 48172

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:53cf288c75a59cb97d2c0d648273e233b0ac9ba41f04b2cc57f7fe24657cf924 oid sha256:5e26bfb2260902eca919bf76eb85f48108acb60991f3347b727b4f28b5415448
size 47069 size 47244

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1e09986f8b500061cfc9108af757b581334d8babb3c5c17433dd53dd3a8f52f3 oid sha256:f52cf95e22042776bb61b7e01810d41e4f1f8b6eaf0319b40c5ea957f1a8603e
size 48709 size 50251

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1e09986f8b500061cfc9108af757b581334d8babb3c5c17433dd53dd3a8f52f3 oid sha256:f52cf95e22042776bb61b7e01810d41e4f1f8b6eaf0319b40c5ea957f1a8603e
size 48709 size 50251

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f953609cb044e439198f7072a4468c54269b646e3c39686440334903eb12797d oid sha256:9e223457eafee936749ba938e2dc8ccefb64bae01510af8a6cb1c8bf59646b59
size 49279 size 50829

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c481049fcd33ca40aac538bf82ca56ef27cce2448c148cc45ba95493b668ef03 oid sha256:cba57ac31d14044e0380ea9e81114cb2925565a955c6254f24c2dcb2e61662cb
size 47890 size 49180

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c481049fcd33ca40aac538bf82ca56ef27cce2448c148cc45ba95493b668ef03 oid sha256:cba57ac31d14044e0380ea9e81114cb2925565a955c6254f24c2dcb2e61662cb
size 47890 size 49180

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:15ed6659fbf148a23b4090ba8896bad91d82279df71552c2798755b190a0cdca oid sha256:dbfc4dac4fd2139132348e3d07fa1713f3047c210f2cfc824044a1bf657b0b3e
size 48369 size 49665

Some files were not shown because too many files have changed in this diff Show more