diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedItem.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedItem.kt new file mode 100644 index 0000000000..d72d786b8d --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedItem.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.text.toPx +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun SelectedItem( + avatarData: AvatarData, + avatarType: AvatarType, + text: String, + maxLines: Int, + a11yContentDescription: String, + canRemove: Boolean, + onRemoveClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val actionRemove = stringResource(id = CommonStrings.action_remove) + Box( + modifier = modifier + .width(avatarData.size.dp) + .clearAndSetSemantics { + contentDescription = a11yContentDescription + if (canRemove) { + // Note: this does not set the click effect to the whole Box + // when talkback is not enabled + onClick( + label = actionRemove, + action = { + onRemoveClick() + true + } + ) + } + } + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + val closeIconRadius = 12.dp.toPx() + val closeIconOffset = 10.dp.toPx() + Avatar( + avatarData = avatarData, + avatarType = avatarType, + modifier = Modifier + .graphicsLayer { + compositingStrategy = CompositingStrategy.Offscreen + } + .drawWithContent { + drawContent() + if (canRemove) { + val xOffset = if (isRtl) { + closeIconOffset + } else { + size.width - closeIconOffset + } + drawCircle( + color = Color.Black, + center = Offset( + x = xOffset, + y = closeIconOffset, + ), + radius = closeIconRadius, + blendMode = BlendMode.Clear, + ) + } + }, + ) + Text( + modifier = Modifier.clipToBounds(), + text = text, + overflow = TextOverflow.Ellipsis, + maxLines = maxLines, + style = MaterialTheme.typography.bodyMedium, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + } + if (canRemove) { + Surface( + color = ElementTheme.colors.bgActionPrimaryRest, + modifier = Modifier + .clip(CircleShape) + .size(20.dp) + .align(Alignment.TopEnd) + .clickable( + indication = ripple(), + interactionSource = remember { MutableInteractionSource() }, + onClick = onRemoveClick, + ), + ) { + Icon( + imageVector = CompoundIcons.Close(), + // Note: keep the context description for the test + contentDescription = stringResource(id = CommonStrings.action_remove), + tint = ElementTheme.colors.iconOnSolidPrimary, + modifier = Modifier.padding(2.dp) + ) + } + } + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt index bf6db009a7..59542a5338 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt @@ -7,48 +7,17 @@ package io.element.android.libraries.matrix.ui.components -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.onClick -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.text.toPx -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Surface -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.ui.model.SelectRoomInfo import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.ui.strings.CommonStrings @@ -60,89 +29,22 @@ fun SelectedRoom( onRemoveRoom: (SelectRoomInfo) -> Unit, modifier: Modifier = Modifier, ) { - val actionRemove = stringResource(id = CommonStrings.action_remove) - val a11yRoomName = stringResource(id = CommonStrings.common_room_name) - Box( - modifier = modifier - .width(AvatarSize.SelectedRoom.dp) - .clearAndSetSemantics { - contentDescription = roomInfo.name - ?: roomInfo.canonicalAlias?.value - ?: a11yRoomName - // Note: this does not set the click effect to the whole Box - // when talkback is not enabled - onClick( - label = actionRemove, - action = { - onRemoveRoom(roomInfo) - true - } - ) - } - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - val closeIconRadius = 12.dp.toPx() - val closeIconOffset = 10.dp.toPx() - Avatar( - modifier = Modifier - .graphicsLayer { - compositingStrategy = CompositingStrategy.Offscreen - } - .drawWithContent { - drawContent() - val xOffset = if (isRtl) { - closeIconOffset - } else { - size.width - closeIconOffset - } - drawCircle( - color = Color.Black, - center = Offset( - x = xOffset, - y = closeIconOffset, - ), - radius = closeIconRadius, - blendMode = BlendMode.Clear, - ) - }, - avatarData = roomInfo.getAvatarData(AvatarSize.SelectedRoom), - avatarType = AvatarType.Room( - heroes = roomInfo.heroes.map { it.getAvatarData(AvatarSize.SelectedRoom) }.toImmutableList(), - isTombstoned = roomInfo.isTombstoned, - ), - ) - Text( - // If name is null, we do not have space to render "No room name", so just use `#` here. - text = roomInfo.name ?: "#", - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = MaterialTheme.typography.bodyMedium, - color = ElementTheme.colors.textSecondary, - ) - } - Surface( - color = ElementTheme.colors.bgActionPrimaryRest, - modifier = Modifier - .clip(CircleShape) - .size(20.dp) - .align(Alignment.TopEnd) - .clickable( - indication = ripple(), - interactionSource = remember { MutableInteractionSource() }, - onClick = { onRemoveRoom(roomInfo) } - ), - ) { - Icon( - imageVector = CompoundIcons.Close(), - contentDescription = stringResource(id = CommonStrings.action_remove), - tint = ElementTheme.colors.iconOnSolidPrimary, - modifier = Modifier.padding(2.dp) - ) - } - } + SelectedItem( + avatarData = roomInfo.getAvatarData(AvatarSize.SelectedRoom), + avatarType = AvatarType.Room( + heroes = roomInfo.heroes.map { it.getAvatarData(AvatarSize.SelectedRoom) }.toImmutableList(), + isTombstoned = roomInfo.isTombstoned, + ), + // If name is null, we do not have space to render "No room name", so just use `#` here. + text = roomInfo.name ?: "#", + maxLines = 1, + a11yContentDescription = roomInfo.name + ?: roomInfo.canonicalAlias?.value + ?: stringResource(id = CommonStrings.common_room_name), + canRemove = true, + onRemoveClick = { onRemoveRoom(roomInfo) }, + modifier = modifier, + ) } @PreviewsDayNight diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt index 28591c5eca..d8648c903e 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt @@ -7,54 +7,19 @@ package io.element.android.libraries.matrix.ui.components -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.onClick -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.text.toPx -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Surface -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getBestName -import io.element.android.libraries.ui.strings.CommonStrings @Composable fun SelectedUser( @@ -63,91 +28,16 @@ fun SelectedUser( onUserRemove: (MatrixUser) -> Unit, modifier: Modifier = Modifier, ) { - val actionRemove = stringResource(id = CommonStrings.action_remove) - Box( - modifier = modifier - .width(AvatarSize.SelectedUser.dp) - .clearAndSetSemantics { - contentDescription = matrixUser.getBestName() - if (canRemove) { - // Note: this does not set the click effect to the whole Box - // when talkback is not enabled - onClick( - label = actionRemove, - action = { - onUserRemove(matrixUser) - true - } - ) - } - } - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - val closeIconRadius = 12.dp.toPx() - val closeIconOffset = 10.dp.toPx() - Avatar( - avatarData = matrixUser.getAvatarData(size = AvatarSize.SelectedUser), - avatarType = AvatarType.User, - modifier = Modifier - .graphicsLayer { - compositingStrategy = CompositingStrategy.Offscreen - } - .drawWithContent { - drawContent() - if (canRemove) { - val xOffset = if (isRtl) { - closeIconOffset - } else { - size.width - closeIconOffset - } - drawCircle( - color = Color.Black, - center = Offset( - x = xOffset, - y = closeIconOffset, - ), - radius = closeIconRadius, - blendMode = BlendMode.Clear, - ) - } - } - ) - Text( - modifier = Modifier.clipToBounds(), - text = matrixUser.getBestName(), - overflow = TextOverflow.Ellipsis, - maxLines = 2, - style = MaterialTheme.typography.bodyMedium, - color = ElementTheme.colors.textSecondary, - textAlign = TextAlign.Center, - ) - } - if (canRemove) { - Surface( - color = ElementTheme.colors.bgActionPrimaryRest, - modifier = Modifier - .clip(CircleShape) - .size(20.dp) - .align(Alignment.TopEnd) - .clickable( - indication = ripple(), - interactionSource = remember { MutableInteractionSource() }, - onClick = { onUserRemove(matrixUser) } - ), - ) { - Icon( - imageVector = CompoundIcons.Close(), - // Note: keep the context description for the test - contentDescription = stringResource(id = CommonStrings.action_remove), - tint = ElementTheme.colors.iconOnSolidPrimary, - modifier = Modifier.padding(2.dp) - ) - } - } - } + SelectedItem( + avatarData = matrixUser.getAvatarData(size = AvatarSize.SelectedUser), + avatarType = AvatarType.User, + text = matrixUser.getBestName(), + maxLines = 2, + a11yContentDescription = matrixUser.getBestName(), + canRemove = canRemove, + onRemoveClick = { onUserRemove(matrixUser) }, + modifier = modifier, + ) } @PreviewsDayNight