Skip to content

Commit 3391cfb

Browse files
committed
1 parent 9bc2c4a commit 3391cfb

File tree

3 files changed

+166
-3
lines changed

3 files changed

+166
-3
lines changed

libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ open class AvatarDataProvider : PreviewParameterProvider<AvatarData> {
1616
.map {
1717
sequenceOf(
1818
anAvatarData(size = it),
19-
anAvatarData(size = it).copy(name = null),
20-
anAvatarData(size = it).copy(url = "aUrl"),
19+
anAvatarData(size = it, name = null),
20+
anAvatarData(size = it, url = "aUrl"),
2121
)
2222
}
2323
.flatten()
@@ -26,10 +26,12 @@ open class AvatarDataProvider : PreviewParameterProvider<AvatarData> {
2626
fun anAvatarData(
2727
// Let's the id not start with a 'a'.
2828
id: String = "@id_of_alice:server.org",
29-
name: String = "Alice",
29+
name: String? = "Alice",
30+
url: String? = null,
3031
size: AvatarSize = AvatarSize.RoomListItem,
3132
) = AvatarData(
3233
id = id,
3334
name = name,
35+
url = url,
3436
size = size,
3537
)

libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,6 @@ enum class AvatarSize(val dp: Dp) {
6363
DmCreationConfirmation(64.dp),
6464

6565
UserVerification(52.dp),
66+
67+
OrganizationHeader(64.dp),
6668
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.matrix.ui.components
9+
10+
import androidx.compose.foundation.border
11+
import androidx.compose.foundation.clickable
12+
import androidx.compose.foundation.interaction.MutableInteractionSource
13+
import androidx.compose.foundation.layout.Box
14+
import androidx.compose.foundation.layout.padding
15+
import androidx.compose.foundation.layout.size
16+
import androidx.compose.foundation.layout.width
17+
import androidx.compose.foundation.shape.CircleShape
18+
import androidx.compose.material3.ripple
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.CompositionLocalProvider
21+
import androidx.compose.runtime.remember
22+
import androidx.compose.ui.Alignment
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.draw.clip
25+
import androidx.compose.ui.draw.drawWithContent
26+
import androidx.compose.ui.geometry.Offset
27+
import androidx.compose.ui.graphics.BlendMode
28+
import androidx.compose.ui.graphics.Color
29+
import androidx.compose.ui.graphics.CompositingStrategy
30+
import androidx.compose.ui.graphics.graphicsLayer
31+
import androidx.compose.ui.platform.LocalLayoutDirection
32+
import androidx.compose.ui.res.stringResource
33+
import androidx.compose.ui.semantics.clearAndSetSemantics
34+
import androidx.compose.ui.semantics.contentDescription
35+
import androidx.compose.ui.semantics.onClick
36+
import androidx.compose.ui.unit.LayoutDirection
37+
import androidx.compose.ui.unit.dp
38+
import io.element.android.compound.theme.ElementTheme
39+
import io.element.android.compound.tokens.generated.CompoundIcons
40+
import io.element.android.libraries.designsystem.components.avatar.Avatar
41+
import io.element.android.libraries.designsystem.components.avatar.AvatarData
42+
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
43+
import io.element.android.libraries.designsystem.components.avatar.AvatarType
44+
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
45+
import io.element.android.libraries.designsystem.preview.ElementPreview
46+
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
47+
import io.element.android.libraries.designsystem.text.toPx
48+
import io.element.android.libraries.designsystem.theme.components.Icon
49+
import io.element.android.libraries.designsystem.theme.components.Surface
50+
import io.element.android.libraries.ui.strings.CommonStrings
51+
52+
/**
53+
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=3643-2678&m=dev
54+
*/
55+
@Composable
56+
fun EditableOrgAvatar(
57+
avatarData: AvatarData,
58+
onEdit: () -> Unit,
59+
modifier: Modifier = Modifier,
60+
) {
61+
val actionEdit = stringResource(id = CommonStrings.action_edit)
62+
val description = stringResource(CommonStrings.a11y_avatar)
63+
Box(
64+
modifier = modifier
65+
.width(avatarData.size.dp + 16.dp)
66+
.clearAndSetSemantics {
67+
contentDescription = description
68+
// Note: this does not set the click effect to the whole Box
69+
// when talkback is not enabled
70+
onClick(
71+
label = actionEdit,
72+
action = {
73+
onEdit()
74+
true
75+
}
76+
)
77+
}
78+
) {
79+
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
80+
val editIconRadius = 17.dp.toPx()
81+
val editIconXOffset = 7.dp.toPx()
82+
val editIconYOffset = 15.dp.toPx()
83+
Avatar(
84+
avatarData = avatarData,
85+
avatarType = AvatarType.Space(false),
86+
modifier = Modifier
87+
.align(Alignment.Center)
88+
.graphicsLayer {
89+
compositingStrategy = CompositingStrategy.Offscreen
90+
}
91+
.drawWithContent {
92+
drawContent()
93+
val xOffset = if (isRtl) {
94+
editIconXOffset
95+
} else {
96+
size.width - editIconXOffset
97+
}
98+
drawCircle(
99+
color = Color.Black,
100+
center = Offset(
101+
x = xOffset,
102+
y = size.height - editIconYOffset,
103+
),
104+
radius = editIconRadius,
105+
blendMode = BlendMode.Clear,
106+
)
107+
},
108+
)
109+
Surface(
110+
color = ElementTheme.colors.bgCanvasDefault,
111+
modifier = Modifier
112+
.clip(CircleShape)
113+
.size(30.dp)
114+
.border(1.dp, color = ElementTheme.colors.borderInteractiveSecondary, shape = CircleShape)
115+
.align(Alignment.BottomEnd)
116+
.clickable(
117+
indication = ripple(),
118+
interactionSource = remember { MutableInteractionSource() },
119+
onClick = onEdit,
120+
),
121+
) {
122+
Icon(
123+
imageVector = CompoundIcons.Edit(),
124+
// Note: keep the context description for the test
125+
contentDescription = stringResource(id = CommonStrings.action_edit),
126+
tint = ElementTheme.colors.iconPrimary,
127+
modifier = Modifier.padding(6.dp)
128+
)
129+
}
130+
}
131+
}
132+
133+
@PreviewsDayNight
134+
@Composable
135+
internal fun EditableOrgAvatarPreview() = ElementPreview {
136+
EditableOrgAvatar(
137+
avatarData = anAvatarData(
138+
url = "anUrl",
139+
size = AvatarSize.OrganizationHeader,
140+
),
141+
onEdit = {},
142+
)
143+
}
144+
145+
@PreviewsDayNight
146+
@Composable
147+
internal fun EditableOrgAvatarRtlPreview() = CompositionLocalProvider(
148+
LocalLayoutDirection provides LayoutDirection.Rtl,
149+
) {
150+
ElementPreview {
151+
EditableOrgAvatar(
152+
avatarData = anAvatarData(
153+
url = "anUrl",
154+
size = AvatarSize.OrganizationHeader,
155+
),
156+
onEdit = {},
157+
)
158+
}
159+
}

0 commit comments

Comments
 (0)