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 b271e06973
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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 var pendingPermissionRequest = false
init {
dataStore.setIsSpace(isSpace)
}
@Composable
override fun present(): ConfigureRoomState {
val canAddRoomToSpace by featureFlagService.isFeatureEnabledFlow(FeatureFlags.CreateSpaces).collectAsState(false)
@ -123,9 +119,10 @@ class ConfigureRoomPresenter(
} else {
persistentListOf()
}
val parentSpace = spaces.find { it.roomId == initialParentSpaceId }
parentSpace?.let { dataStore.setParentSpace(it) }
parentSpace?.let {
dataStore.setParentSpace(parentSpace = parentSpace, updateVisibility = true)
}
}
LaunchedEffect(cameraPermissionState.permissionGranted) {
@ -152,21 +149,42 @@ class ConfigureRoomPresenter(
// 2. If it has a parent space.
// 3. If knocking is enabled.
val parentSpace = createRoomConfig.parentSpace
val availableJoinRules = remember(createRoomConfig.parentSpace, isSpace, isKnockFeatureEnabled) {
val availableJoinRules = remember(parentSpace, isSpace, isKnockFeatureEnabled) {
when {
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(
JoinRuleItem.PublicVisibility.Public,
JoinRuleItem.PublicVisibility.AskToJoin.takeIf { !isSpace && isKnockFeatureEnabled },
JoinRuleItem.Private,
JoinRuleItem.PrivateVisibility.Private,
).toImmutableList()
else -> listOfNotNull(
JoinRuleItem.PublicVisibility.Restricted(parentSpace.roomId),
JoinRuleItem.PublicVisibility.AskToJoinRestricted(parentSpace.roomId).takeIf { !isSpace && isKnockFeatureEnabled },
JoinRuleItem.Private,
JoinRuleItem.PrivateVisibility.Restricted(parentSpace.roomId),
JoinRuleItem.PrivateVisibility.AskToJoinRestricted(parentSpace.roomId).takeIf { isKnockFeatureEnabled },
JoinRuleItem.PrivateVisibility.Private,
).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) {
createRoomAction.value = AsyncAction.Uninitialized
@ -193,7 +211,7 @@ class ConfigureRoomPresenter(
}
}
is ConfigureRoomEvents.SetParentSpace -> {
dataStore.setParentSpace(event.space)
dataStore.setParentSpace(event.space, false)
}
ConfigureRoomEvents.CancelCreateRoom -> {
createRoomAction.value = AsyncAction.Uninitialized
@ -210,6 +228,7 @@ class ConfigureRoomPresenter(
roomAddressValidity = roomAddressValidity.value,
availableJoinRules = availableJoinRules,
spaces = spaces,
isSpace = isSpace,
eventSink = ::handleEvent,
)
}
@ -220,35 +239,41 @@ class ConfigureRoomPresenter(
) = launch {
suspend {
val avatarUrl = config.avatarUri?.let { uploadAvatar(it.toUri()) }
val params = if (config.visibilityState is RoomVisibilityState.Public) {
CreateRoomParameters(
name = config.roomName,
topic = config.topic,
isEncrypted = false,
isDirect = false,
visibility = RoomVisibility.Public,
joinRuleOverride = config.visibilityState.joinRuleItem.toJoinRule()
// No need to specify the public join rule override, since the preset is already PUBLIC_CHAT
.takeIf { it != JoinRule.Public },
preset = RoomPreset.PUBLIC_CHAT,
invite = config.invites.map { it.userId },
avatar = avatarUrl,
roomAliasName = config.visibilityState.roomAddress(),
isSpace = isSpace,
)
} else {
CreateRoomParameters(
name = config.roomName,
topic = config.topic,
isEncrypted = config.visibilityState is RoomVisibilityState.Private,
isDirect = false,
visibility = RoomVisibility.Private,
historyVisibilityOverride = RoomHistoryVisibility.Invited,
preset = RoomPreset.PRIVATE_CHAT,
invite = config.invites.map { it.userId },
avatar = avatarUrl,
isSpace = isSpace,
)
val params = when (config.visibilityState) {
is RoomVisibilityState.Public -> {
CreateRoomParameters(
name = config.roomName,
topic = config.topic,
isEncrypted = false,
isDirect = false,
visibility = RoomVisibility.Public,
joinRuleOverride = config.visibilityState.joinRuleItem.toJoinRule()
// No need to specify the public join rule override, since the preset is already PUBLIC_CHAT
.takeIf { it != JoinRule.Public },
preset = RoomPreset.PUBLIC_CHAT,
invite = config.invites.map { it.userId },
avatar = avatarUrl,
roomAliasName = config.visibilityState.roomAddress(),
isSpace = isSpace,
)
}
is RoomVisibilityState.Private -> {
CreateRoomParameters(
name = config.roomName,
topic = config.topic,
isEncrypted = true,
isDirect = false,
visibility = RoomVisibility.Private,
historyVisibilityOverride = RoomHistoryVisibility.Invited,
joinRuleOverride = config.visibilityState.joinRuleItem.toJoinRule()
// 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)
.onFailure { failure ->

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.permissions.api.PermissionsState
import kotlinx.collections.immutable.ImmutableList
data class ConfigureRoomState(
val isSpace: Boolean,
val config: CreateRoomConfig,
val avatarActions: ImmutableList<AvatarAction>,
val createRoomAction: AsyncAction<RoomId>,
@ -28,5 +29,6 @@ data class ConfigureRoomState(
val eventSink: (ConfigureRoomEvents) -> Unit
) {
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,
),
aConfigureRoomState(
isSpace = true,
config = CreateRoomConfig(
isSpace = true,
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",
visibilityState = RoomVisibilityState.Public(
@ -95,13 +95,11 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
),
aConfigureRoomState(
config = CreateRoomConfig(
isSpace = false,
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",
parentSpace = null,
visibilityState = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Space-101"),
joinRuleItem = JoinRuleItem.PublicVisibility.Restricted(aSpaceRoom().roomId),
visibilityState = RoomVisibilityState.Private(
joinRuleItem = JoinRuleItem.PrivateVisibility.Restricted(aSpaceRoom().roomId),
),
),
spaces = listOf(aSpaceRoom()),
@ -109,13 +107,11 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
),
aConfigureRoomState(
config = CreateRoomConfig(
isSpace = false,
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",
parentSpace = aSpaceRoom(canonicalAlias = RoomAlias("#a-space-room:example.org")),
visibilityState = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Space-101"),
joinRuleItem = JoinRuleItem.PublicVisibility.Restricted(aSpaceRoom().roomId),
visibilityState = RoomVisibilityState.Private(
joinRuleItem = JoinRuleItem.PrivateVisibility.Restricted(aSpaceRoom().roomId),
),
),
spaces = listOf(aSpaceRoom()),
@ -126,6 +122,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
fun aConfigureRoomState(
config: CreateRoomConfig = CreateRoomConfig(),
isSpace: Boolean = false,
isKnockFeatureEnabled: Boolean = true,
avatarActions: List<AvatarAction> = emptyList(),
createRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
@ -134,21 +131,22 @@ fun aConfigureRoomState(
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
availableVisibilityOptions: List<JoinRuleItem> = if (config.parentSpace != null) {
listOfNotNull(
JoinRuleItem.PublicVisibility.Restricted(config.parentSpace.roomId),
JoinRuleItem.PublicVisibility.AskToJoinRestricted(config.parentSpace.roomId).takeIf { isKnockFeatureEnabled },
JoinRuleItem.Private,
JoinRuleItem.PrivateVisibility.Restricted(config.parentSpace.roomId),
JoinRuleItem.PrivateVisibility.AskToJoinRestricted(config.parentSpace.roomId).takeIf { isKnockFeatureEnabled },
JoinRuleItem.PrivateVisibility.Private,
)
} else {
listOfNotNull(
JoinRuleItem.PublicVisibility.Public,
JoinRuleItem.PublicVisibility.AskToJoin.takeIf { isKnockFeatureEnabled },
JoinRuleItem.Private,
JoinRuleItem.PrivateVisibility.Private,
)
},
spaces: List<SpaceRoom> = emptyList(),
eventSink: (ConfigureRoomEvents) -> Unit = { },
) = ConfigureRoomState(
config = config,
isSpace = isSpace,
avatarActions = avatarActions.toImmutableList(),
createRoomAction = createRoomAction,
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -76,7 +78,7 @@ fun ConfigureRoomView(
onCreateRoomSuccess: (RoomId) -> Unit,
modifier: Modifier = Modifier,
) {
val isSpace = state.config.isSpace
val isSpace = state.isSpace
val focusManager = LocalFocusManager.current
val isAvatarActionsSheetVisible = remember { mutableStateOf(false) }
@ -105,7 +107,6 @@ fun ConfigureRoomView(
.imePadding()
.verticalScroll(rememberScrollState())
.consumeWindowInsets(padding),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
RoomNameWithAvatar(
isSpace = isSpace,
@ -115,20 +116,20 @@ fun ConfigureRoomView(
onAvatarClick = ::onAvatarClick,
onChangeRoomName = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
)
Spacer(modifier = Modifier.height(16.dp))
RoomTopic(
modifier = Modifier.padding(horizontal = 16.dp),
topic = state.config.topic.orEmpty(),
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
)
if (!state.config.isSpace && state.spaces.isNotEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
if (!state.isSpace && state.spaces.isNotEmpty()) {
SelectParentSpaceOptions(
spaces = state.spaces,
selectedSpace = state.config.parentSpace,
onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) },
)
}
RoomJoinRuleOptions(
options = state.availableJoinRules,
selected = state.config.visibilityState.joinRuleItem,
@ -138,20 +139,17 @@ fun ConfigureRoomView(
state.eventSink(ConfigureRoomEvents.JoinRuleChanged(it))
},
)
if (state.config.visibilityState !is RoomVisibilityState.Private) {
Column {
ListSectionHeader(title = stringResource(R.string.screen_create_room_room_address_section_title))
RoomAddressField(
modifier = Modifier.padding(horizontal = 16.dp),
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
homeserverName = state.homeserverName,
addressValidity = state.roomAddressValidity,
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
label = null,
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
)
}
ListSectionHeader(title = stringResource(R.string.screen_create_room_room_address_section_title))
RoomAddressField(
modifier = Modifier.padding(horizontal = 16.dp),
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
homeserverName = state.homeserverName,
addressValidity = state.roomAddressValidity,
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
label = null,
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
)
}
}
}
@ -217,7 +215,9 @@ private fun RoomNameWithAvatar(
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier.padding(end = 8.dp).size(AvatarSize.EditRoomDetails.dp),
modifier = Modifier
.padding(end = 8.dp)
.size(AvatarSize.EditRoomDetails.dp),
contentAlignment = Alignment.Center,
) {
val avatarState = remember(avatarUri) {
@ -272,12 +272,13 @@ private fun RoomTopic(
internal fun ConfigureRoomOptions(
title: String,
modifier: Modifier = Modifier,
hasDivider: Boolean = true,
content: @Composable ColumnScope.() -> Unit,
) {
Column(
modifier = modifier.selectableGroup()
) {
ListSectionHeader(title = title)
ListSectionHeader(title = title, hasDivider = hasDivider)
content()
}
}
@ -302,10 +303,10 @@ private fun RoomJoinRuleOptions(
size = RoundedIconAtomSize.Big,
imageVector = when (item) {
JoinRuleItem.PublicVisibility.Public -> CompoundIcons.Public()
is JoinRuleItem.PublicVisibility.Restricted -> CompoundIcons.Space()
is JoinRuleItem.PrivateVisibility.Restricted -> CompoundIcons.Space()
JoinRuleItem.PublicVisibility.AskToJoin,
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> CompoundIcons.UserAdd()
JoinRuleItem.Private -> CompoundIcons.Lock()
is JoinRuleItem.PrivateVisibility.AskToJoinRestricted -> CompoundIcons.UserAdd()
JoinRuleItem.PrivateVisibility.Private -> CompoundIcons.Lock()
},
tint = if (isSelected) ElementTheme.colors.iconPrimary else ElementTheme.colors.iconSecondary,
backgroundTint = Color.Transparent,
@ -314,28 +315,28 @@ private fun RoomJoinRuleOptions(
headlineContent = {
val title = when (item) {
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)
is JoinRuleItem.PublicVisibility.AskToJoinRestricted -> stringResource(
is JoinRuleItem.PrivateVisibility.AskToJoinRestricted -> stringResource(
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)
},
supportingContent = {
val description = when (item) {
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,
parentSpace?.displayName.orEmpty()
)
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,
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)
},

View file

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

View file

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

View file

@ -18,7 +18,11 @@ import kotlinx.collections.immutable.persistentListOf
*/
@Immutable
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.
@ -27,18 +31,16 @@ sealed interface JoinRuleItem {
sealed interface PublicVisibility : JoinRuleItem {
data object Public : 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].
*/
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.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 {
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(
val roomAddress: RoomAddress,

View file

@ -8,7 +8,6 @@
package io.element.android.features.createroom.impl.configureroom
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
@ -21,7 +20,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
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.libraries.designsystem.components.avatar.Avatar
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.preview.ElementPreview
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.ListSectionHeader
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
@ -55,6 +53,7 @@ internal fun SelectParentSpaceOptions(
var displaySelectSpaceBottomSheet by remember { mutableStateOf(false) }
ConfigureRoomOptions(
title = stringResource(CommonStrings.common_space),
hasDivider = false,
modifier = modifier
) {
ListItem(
@ -62,22 +61,16 @@ internal fun SelectParentSpaceOptions(
Text(
text = selectedSpace?.displayName
?: stringResource(R.string.screen_create_room_space_selection_no_space_title),
maxLines = 1
maxLines = 1,
color = ElementTheme.colors.textPrimary
)
},
supportingContent = {
Text(
text = if (selectedSpace != null) {
selectedSpace.canonicalAlias?.value.orEmpty()
} else {
stringResource(R.string.screen_create_room_space_selection_no_space_description)
},
maxLines = 1
)
supportingContent = selectedSpace?.canonicalAlias?.let { alias ->
{
Text(text = alias.value, maxLines = 1)
}
},
leadingContent = if (selectedSpace == null) {
ListItemContent.Icon(IconSource.Vector(CompoundIcons.Home()))
} else {
leadingContent = selectedSpace?.let {
ListItemContent.Custom({
val avatarData = AvatarData(
id = selectedSpace.roomId.value,
@ -119,7 +112,7 @@ internal fun SelectParentSpaceOptions(
}
@Composable
private fun ColumnScope.SelectParentSpaceBottomSheet(
private fun SelectParentSpaceBottomSheet(
spaces: ImmutableList<SpaceRoom>,
selectedSpace: SpaceRoom?,
onSelectSpace: (SpaceRoom?) -> Unit,
@ -133,19 +126,10 @@ private fun ColumnScope.SelectParentSpaceBottomSheet(
ListItem(
headlineContent = {
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
)
},
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(
selected = selectedSpace == null
),
@ -157,29 +141,31 @@ private fun ColumnScope.SelectParentSpaceBottomSheet(
ListItem(
headlineContent = {
Text(
space.displayName,
text = space.displayName,
maxLines = 1
)
},
supportingContent = {
Text(
space.canonicalAlias?.value.orEmpty(),
maxLines = 1
)
supportingContent = space.canonicalAlias?.let { alias ->
{
Text(
text = alias.value,
maxLines = 1
)
}
},
leadingContent = ListItemContent.Custom({
val avatarData =
AvatarData(
id = space.roomId.value,
name = space.displayName,
url = space.avatarUrl,
size = AvatarSize.SelectParentSpace,
)
Avatar(
avatarData = avatarData,
avatarType = AvatarType.Space()
val avatarData =
AvatarData(
id = space.roomId.value,
name = space.displayName,
url = space.avatarUrl,
size = AvatarSize.SelectParentSpace,
)
}),
Avatar(
avatarData = avatarData,
avatarType = AvatarType.Space()
)
}),
trailingContent = ListItemContent.RadioButton(
selected = selectedSpace == space
),
@ -201,7 +187,8 @@ internal fun SelectParentSpaceBottomSheetPreview() =
canonicalAlias = RoomAlias(
"#a-room-alias:example.org"
)
)
),
aSpaceRoom()
),
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.alias.ResolvedRoomAlias
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.RoomPowerLevelsValues
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
@ -89,7 +90,7 @@ class ConfigureRoomPresenterTest {
assertThat(initialState.config.topic).isNull()
assertThat(initialState.config.invites).isEmpty()
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.homeserverName).isEqualTo("matrix.org")
}
@ -234,7 +235,8 @@ class ConfigureRoomPresenterTest {
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))
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
@ -275,7 +277,8 @@ class ConfigureRoomPresenterTest {
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))
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
@ -484,16 +487,19 @@ class ConfigureRoomPresenterTest {
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
// 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()))
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
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))
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 {
@Test
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.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))))
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))))
}
}

View file

@ -76,6 +76,7 @@ fun HomeSpacesView(
item {
SpaceHeaderView(
avatarData = space.spaceRoom.getAvatarData(AvatarSize.SpaceHeader),
alias = space.spaceRoom.canonicalAlias,
name = space.spaceRoom.displayName,
topic = space.spaceRoom.topic,
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.features.invite.api.InviteData
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.RoomPreviewSubtitleAtom
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.IconTitlePlaceholdersRowMolecule
@ -514,7 +514,7 @@ private fun IncompleteContent(
title = {
when (roomIdOrAlias) {
is RoomIdOrAlias.Alias -> {
RoomPreviewSubtitleAtom(roomIdOrAlias.identifier)
RoomPreviewAliasAtom(roomIdOrAlias.identifier)
}
is RoomIdOrAlias.Id -> {
PlaceholderAtom(width = 200.dp, height = 22.dp)
@ -566,13 +566,12 @@ private fun DefaultLoadedContent(
}
},
subtitle = {
when {
contentState.details is LoadedDetails.Space -> {
SpaceInfoRow(visibility = SpaceRoomVisibility.fromJoinRule(contentState.joinRule))
}
contentState.alias != null -> {
RoomPreviewSubtitleAtom(contentState.alias.value)
}
if (contentState.alias != null) {
RoomPreviewAliasAtom(contentState.alias.value)
}
if (contentState.details is LoadedDetails.Space) {
Spacer(Modifier.height(8.dp))
SpaceInfoRow(visibility = SpaceRoomVisibility.fromJoinRule(contentState.joinRule))
}
},
description = {

View file

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

View file

@ -6,6 +6,8 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(FlowPreview::class)
package io.element.android.features.space.impl.root
import androidx.compose.runtime.Composable
@ -47,10 +49,13 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
@Inject
class SpacePresenter(
@ -80,13 +85,16 @@ class SpacePresenter(
val localCoroutineScope = rememberCoroutineScope()
val hasMoreToLoad by remember {
spaceRoomList.paginationStatusFlow.mapState { status ->
when (status) {
is SpaceRoomList.PaginationStatus.Idle -> status.hasMoreToLoad
SpaceRoomList.PaginationStatus.Loading -> true
spaceRoomList.paginationStatusFlow
.mapState { status ->
when (status) {
is SpaceRoomList.PaginationStatus.Idle -> status.hasMoreToLoad
SpaceRoomList.PaginationStatus.Loading -> true
}
}
}
}.collectAsState()
// Debounce to give more time for spaceRoomList to updates
.debounce(100.milliseconds)
}.collectAsState(true)
val permissions by room.permissionsAsState(SpacePermissions.DEFAULT) { perms ->
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.features.space.impl.R
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.InviteButtonsRowMolecule
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.IconButton
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.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
@ -170,6 +172,7 @@ fun SpaceView(
state.eventSink(SpaceEvents.ShowTopicViewer(topic))
},
onCreateRoomClick = onCreateRoomClick,
onAddRoomClick = onAddRoomClick,
)
JoinFailuresEffect(
hasAnyFailure = state.hasAnyJoinFailures,
@ -243,6 +246,7 @@ private fun SpaceViewContent(
onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
onTopicClick: (String) -> Unit,
onCreateRoomClick: () -> Unit,
onAddRoomClick: () -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier.fillMaxSize()) {
@ -256,6 +260,7 @@ private fun SpaceViewContent(
Column {
SpaceHeaderView(
avatarData = spaceInfo.getAvatarData(AvatarSize.SpaceHeader),
alias = spaceInfo.canonicalAlias,
name = spaceInfo.name,
topic = spaceInfo.topic,
topicMaxLines = 2,
@ -271,7 +276,10 @@ private fun SpaceViewContent(
if (state.children.isEmpty() && state.canEditSpaceGraph && !state.hasMoreToLoad) {
item {
EmptySpaceView(onCreateRoomClick = onCreateRoomClick)
EmptySpaceView(
onCreateRoomClick = onCreateRoomClick,
onAddRoomClick = onAddRoomClick,
)
}
} else {
itemsIndexed(
@ -332,7 +340,10 @@ private fun SpaceViewContent(
}
@Composable
private fun EmptySpaceView(onCreateRoomClick: () -> Unit) {
private fun EmptySpaceView(
onCreateRoomClick: () -> Unit,
onAddRoomClick: () -> Unit,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(bottom = 24.dp),
@ -340,15 +351,25 @@ private fun EmptySpaceView(onCreateRoomClick: () -> Unit) {
IconTitleSubtitleMolecule(
title = stringResource(R.string.screen_space_empty_state_title),
subTitle = null,
iconStyle = BigIcon.Style.Default(CompoundIcons.Room()),
iconStyle = BigIcon.Style.Default(vectorIcon = CompoundIcons.Room(), usePrimaryTint = true),
modifier = Modifier.fillMaxWidth()
.padding(top = 40.dp, start = 24.dp, end = 24.dp, bottom = 24.dp),
)
Button(
text = stringResource(R.string.screen_space_add_room_action),
leadingIcon = IconSource.Vector(CompoundIcons.Plus()),
onClick = onCreateRoomClick,
)
ButtonColumnMolecule(
modifier = Modifier.padding(horizontal = 16.dp)
) {
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.performClick
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.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
@ -215,9 +214,25 @@ class SpaceViewTest {
),
onCreateRoomClick = onCreateRoomClick,
)
rule.clickOn(R.string.screen_space_add_room_action)
rule.clickOn(CommonStrings.action_create_room)
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(

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
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -19,12 +20,12 @@ import androidx.compose.ui.unit.dp
@Composable
fun RoomPreviewOrganism(
avatar: @Composable () -> Unit,
title: @Composable () -> Unit,
subtitle: @Composable () -> Unit,
avatar: @Composable ColumnScope.() -> Unit,
title: @Composable ColumnScope.() -> Unit,
subtitle: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
description: @Composable (() -> Unit)? = null,
memberCount: @Composable (() -> Unit)? = null,
description: @Composable (ColumnScope.() -> Unit)? = null,
memberCount: @Composable (ColumnScope.() -> Unit)? = null,
) {
Column(
modifier = modifier.fillMaxWidth(),

View file

@ -8,6 +8,7 @@
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.Row
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.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.AvatarType
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
@ -34,19 +36,26 @@ internal fun SpaceAvatar(
contentDescription: String? = null,
) {
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 {
avatarType.isTombstoned -> TombstonedRoomAvatar(
size = size,
avatarShape = avatarType.avatarShape(size),
modifier = modifier,
avatarShape = avatarShape,
modifier = commonModifier,
contentDescription = contentDescription,
)
else -> InitialOrImageAvatar(
avatarData = avatarData,
hideAvatarImage = hideAvatarImage,
avatarShape = avatarType.avatarShape(size),
avatarShape = avatarShape,
forcedAvatarSize = forcedAvatarSize,
modifier = modifier,
modifier = commonModifier,
contentDescription = contentDescription,
)
}

View file

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

View file

@ -9,13 +9,17 @@
package io.element.android.libraries.matrix.ui.components
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.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
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.RoomPreviewTitleAtom
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.preview.ElementPreview
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.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
@ -39,6 +44,7 @@ import kotlinx.collections.immutable.persistentListOf
fun SpaceHeaderView(
avatarData: AvatarData,
name: String?,
alias: RoomAlias?,
topic: String?,
visibility: SpaceRoomVisibility,
heroes: ImmutableList<MatrixUser>,
@ -66,7 +72,15 @@ fun SpaceHeaderView(
}
},
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()) {
null
@ -100,6 +114,7 @@ internal fun SpaceHeaderViewPreview() = ElementPreview {
url = "anUrl",
size = AvatarSize.SpaceHeader,
),
alias = RoomAlias("#spaceAlias:matrix.org"),
name = "Space name",
topic = "Space topic: " + LoremIpsum(40).values.first(),
topicMaxLines = 2,

View file

@ -117,7 +117,7 @@ internal fun SpaceInfoRowPreview() = ElementPreview {
visibility = SpaceRoomVisibility.Public
)
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.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@ -100,22 +98,16 @@ fun SpaceRoomItemView(
showIndicator = showUnreadIndicator
)
Spacer(modifier = Modifier.height(1.dp))
SubtitleRow(
visibilityIcon = spaceRoom.visibilityIcon(),
subtitle = spaceRoom.subtitle()
)
VisibilityRow(visibility = spaceRoom.visibility)
Spacer(modifier = Modifier.height(1.dp))
val info = spaceRoom.info()
if (info.isNotBlank()) {
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular,
text = info,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular,
text = pluralStringResource(CommonPlurals.common_member_count, spaceRoom.numJoinedMembers, spaceRoom.numJoinedMembers),
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (bottomAction != null) {
Spacer(modifier = Modifier.height(12.dp))
@ -129,29 +121,26 @@ fun SpaceRoomItemView(
}
@Composable
private fun SubtitleRow(
visibilityIcon: ImageVector?,
subtitle: String,
private fun VisibilityRow(
visibility: SpaceRoomVisibility,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
if (visibilityIcon != null) {
Icon(
modifier = Modifier
.size(16.dp)
.padding(end = 4.dp),
imageVector = visibilityIcon,
contentDescription = null,
tint = ElementTheme.colors.iconTertiary,
)
}
Icon(
modifier = Modifier
.size(16.dp)
.padding(end = 4.dp),
imageVector = visibility.icon,
contentDescription = null,
tint = ElementTheme.colors.iconTertiary,
)
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular,
text = subtitle,
text = visibility.label,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
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
@PreviewsDayNight
internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class) spaceRoom: SpaceRoom) = ElementPreview {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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